diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 33a6ef02ace11..54479c5d99c38 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,7 +11,7 @@ Fields marked with (*) are required. Please don't remove the template. ### Preconditions (*) 1. 2. diff --git a/CHANGELOG.md b/CHANGELOG.md index d357d3a67d1ca..4661c4875737d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,415 @@ +2.3.3 +============= +* GitHub issues: + * [#533](https://github.com/magento/magento2/issues/533) -- By default Allow all access in .htaccess (fixed in [magento/graphql-ce#578](https://github.com/magento/graphql-ce/pull/578)) + * [#601](https://github.com/magento/magento2/issues/601) -- Admin tabs js error. (fixed in [magento/graphql-ce#632](https://github.com/magento/graphql-ce/pull/632)) + * [#631](https://github.com/magento/magento2/issues/631) -- Image Management / Editing (fixed in [magento/graphql-ce#634](https://github.com/magento/graphql-ce/pull/634)) + * [#20124](https://github.com/magento/magento2/issues/20124) -- Sort By label is hidden by Shop By Menu on listing page in iphone5 device (fixed in [magento/magento2#20135](https://github.com/magento/magento2/pull/20135)) + * [#21978](https://github.com/magento/magento2/issues/21978) -- Adding product image: File doesn't exist (fixed in [magento/magento2#22020](https://github.com/magento/magento2/pull/22020)) + * [#22045](https://github.com/magento/magento2/issues/22045) -- Instant Purchase on product page not working properly. (fixed in [magento/magento2#22260](https://github.com/magento/magento2/pull/22260)) + * [#22134](https://github.com/magento/magento2/issues/22134) -- Paypal buttons disable issue - Magento 2.3.1 (fixed in [magento/magento2#22260](https://github.com/magento/magento2/pull/22260)) + * [#22249](https://github.com/magento/magento2/issues/22249) -- Configurable Product Gallery Images Out of Order when More than 10 images (fixed in [magento/magento2#22287](https://github.com/magento/magento2/pull/22287)) + * [#22527](https://github.com/magento/magento2/issues/22527) -- Wishlist and compare icon align issue in product listing page (fixed in [magento/magento2#22532](https://github.com/magento/magento2/pull/22532)) + * [#628](https://github.com/magento/magento2/issues/628) -- Feature Request: Parent entities links in child entities grid. (fixed in [magento/graphql-ce#636](https://github.com/magento/graphql-ce/pull/636)) + * [#640](https://github.com/magento/magento2/issues/640) -- [Insight] Files should not be executable (fixed in [magento/graphql-ce#648](https://github.com/magento/graphql-ce/pull/648)) + * [#603](https://github.com/magento/magento2/issues/603) -- 'Continue' button is disabled even though "I've read OSL licence" is checked (fixed in [magento/graphql-ce#653](https://github.com/magento/graphql-ce/pull/653)) + * [#22406](https://github.com/magento/magento2/issues/22406) -- Store view specific labels cut in left navigation menu (fixed in [magento/magento2#22423](https://github.com/magento/magento2/pull/22423)) + * [#19515](https://github.com/magento/magento2/issues/19515) -- Create new order from backend saves the credit card when it is told not to (fixed in [magento/magento2#19767](https://github.com/magento/magento2/pull/19767)) + * [#21473](https://github.com/magento/magento2/issues/21473) -- Form element validation is not triggered when validation rules change (fixed in [magento/magento2#21992](https://github.com/magento/magento2/pull/21992)) + * [#22641](https://github.com/magento/magento2/issues/22641) -- Typo Issue and Missing header title at Customer Sales order grid (fixed in [magento/magento2#22643](https://github.com/magento/magento2/pull/22643)) + * [#22647](https://github.com/magento/magento2/issues/22647) -- In customer account create page word not readable, should use '-' after break to new line In mobile view (fixed in [magento/magento2#22656](https://github.com/magento/magento2/pull/22656)) + * [#22395](https://github.com/magento/magento2/issues/22395) -- config:set -le and -lc short form options don't work (fixed in [magento/magento2#22720](https://github.com/magento/magento2/pull/22720)) + * [#198](https://github.com/magento/magento2/issues/198) -- Better Search Please!! NOW (fixed in [magento/graphql-ce#371](https://github.com/magento/graphql-ce/pull/371)) + * [#436](https://github.com/magento/magento2/issues/436) -- [Feature request]custom option attach an image (fixed in [magento/graphql-ce#445](https://github.com/magento/graphql-ce/pull/445)) + * [#309](https://github.com/magento/magento2/issues/309) -- Terrible UI in Backend (fixed in [magento/graphql-ce#504](https://github.com/magento/graphql-ce/pull/504)) + * [#535](https://github.com/magento/magento2/issues/535) -- A few bugs (fixed in [magento/graphql-ce#650](https://github.com/magento/graphql-ce/pull/650)) + * [#658](https://github.com/magento/magento2/issues/658) -- Inline translate malfunctioning (fixed in [magento/graphql-ce#665](https://github.com/magento/graphql-ce/pull/665) and [magento/graphql-ce#744](https://github.com/magento/graphql-ce/pull/744)) + * [#657](https://github.com/magento/magento2/issues/657) -- Feature Request: Grid paging options at the top and the bottom of the grid. (fixed in [magento/graphql-ce#666](https://github.com/magento/graphql-ce/pull/666)) + * [#12612](https://github.com/magento/magento2/issues/12612) -- Array to String conversion error on checkout page when changin country - how to debug (fixed in [magento/magento2#22558](https://github.com/magento/magento2/pull/22558)) + * [#22556](https://github.com/magento/magento2/issues/22556) -- VatValidator::validate returns error if region in quoteAddress is not set (fixed in [magento/magento2#22558](https://github.com/magento/magento2/pull/22558)) + * [#20843](https://github.com/magento/magento2/issues/20843) -- Uncaught TypeError: panel.addClass is not a function when Swatches are disabled (fixed in [magento/magento2#22560](https://github.com/magento/magento2/pull/22560)) + * [#22636](https://github.com/magento/magento2/issues/22636) -- arrow toggle not changing only showing to down It should be toggle as every where is working (fixed in [magento/magento2#22644](https://github.com/magento/magento2/pull/22644)) + * [#22640](https://github.com/magento/magento2/issues/22640) -- Add tax rule form checkbox design is not as per the magento admin panel checkbox design, It is showing default design (fixed in [magento/magento2#22655](https://github.com/magento/magento2/pull/22655)) + * [#20906](https://github.com/magento/magento2/issues/20906) -- Magento backend catalog "Cost" without currency symbol (fixed in [magento/magento2#22739](https://github.com/magento/magento2/pull/22739)) + * [#22771](https://github.com/magento/magento2/issues/22771) -- Magento 2.3.0 can't change text area field height admin form using Ui component (fixed in [magento/magento2#22779](https://github.com/magento/magento2/pull/22779)) + * [#22788](https://github.com/magento/magento2/issues/22788) -- New Shipment emails do not generate (fixed in [magento/magento2#22791](https://github.com/magento/magento2/pull/22791)) + * [#18651](https://github.com/magento/magento2/issues/18651) -- Tierprice can't save float percentage value (fixed in [magento/magento2#19584](https://github.com/magento/magento2/pull/19584)) + * [#21672](https://github.com/magento/magento2/issues/21672) -- Database Media Storage - Design Config fails to save transactional email logo correctly (fixed in [magento/magento2#21675](https://github.com/magento/magento2/pull/21675) and [magento/magento2#21674](https://github.com/magento/magento2/pull/21674)) + * [#22028](https://github.com/magento/magento2/issues/22028) -- Unable to update products via csv file, when products ids from file are from wide id range (fixed in [magento/magento2#22575](https://github.com/magento/magento2/pull/22575)) + * [#21558](https://github.com/magento/magento2/issues/21558) -- Navigation issue of review from product listing when click on review count (fixed in [magento/magento2#22794](https://github.com/magento/magento2/pull/22794)) + * [#22127](https://github.com/magento/magento2/issues/22127) -- Magento 2.3.0: getSize call on configurable collection leads to exception, if no product filters are applied (fixed in [magento/magento2#22186](https://github.com/magento/magento2/pull/22186)) + * [#22639](https://github.com/magento/magento2/issues/22639) -- Without select attribute click on add attribute it display all selected when add attribute again. (fixed in [magento/magento2#22724](https://github.com/magento/magento2/pull/22724)) + * [#22676](https://github.com/magento/magento2/issues/22676) -- Compare Products counter, and My Wish List counter vertical not aligned (fixed in [magento/magento2#22742](https://github.com/magento/magento2/pull/22742)) + * [#6659](https://github.com/magento/magento2/issues/6659) -- Disabled payment methods show in Customer Dashboard (fixed in [magento/magento2#22850](https://github.com/magento/magento2/pull/22850)) + * [#4628](https://github.com/magento/magento2/issues/4628) -- .lib-font-face mixin - Fixed font formats (fixed in [magento/magento2#22854](https://github.com/magento/magento2/pull/22854)) + * [#3795](https://github.com/magento/magento2/issues/3795) -- Validation messages missing from datepicker form elements (fixed in [magento/magento2#21397](https://github.com/magento/magento2/pull/21397)) + * [#22786](https://github.com/magento/magento2/issues/22786) -- The validation for UPS configurations triggers even if UPS is disabled for checkout (fixed in [magento/magento2#22787](https://github.com/magento/magento2/pull/22787)) + * [#22822](https://github.com/magento/magento2/issues/22822) -- [Shipping] The contact us link isn't showing on order tracking page (fixed in [magento/magento2#22823](https://github.com/magento/magento2/pull/22823)) + * [#21852](https://github.com/magento/magento2/issues/21852) -- Random Error while waiting for package deployed (fixed in [magento/magento2#22607](https://github.com/magento/magento2/pull/22607)) + * [#22563](https://github.com/magento/magento2/issues/22563) -- Parallelised execution of static content deploy is broken on 2.3-develop (fixed in [magento/magento2#22607](https://github.com/magento/magento2/pull/22607)) + * [#22736](https://github.com/magento/magento2/issues/22736) -- Cursor position not in right side of search keyword in search box when click on search again (Mobile issue) (fixed in [magento/magento2#22795](https://github.com/magento/magento2/pull/22795)) + * [#22875](https://github.com/magento/magento2/issues/22875) -- Billing Agreements page title need to be improved (fixed in [magento/magento2#22876](https://github.com/magento/magento2/pull/22876)) + * [#21214](https://github.com/magento/magento2/issues/21214) -- Luma theme Apply Discount Code section design improvement (fixed in [magento/magento2#21215](https://github.com/magento/magento2/pull/21215)) + * [#22143](https://github.com/magento/magento2/issues/22143) -- Varnish health check failing due to presence of id_prefix in env.php (fixed in [magento/magento2#22307](https://github.com/magento/magento2/pull/22307)) + * [#22317](https://github.com/magento/magento2/issues/22317) -- CodeSniffer should not mark correctly aligned DocBlock elements as code style violation. (fixed in [magento/magento2#22444](https://github.com/magento/magento2/pull/22444)) + * [#22396](https://github.com/magento/magento2/issues/22396) -- config:set fails with JSON values (fixed in [magento/magento2#22513](https://github.com/magento/magento2/pull/22513)) + * [#22506](https://github.com/magento/magento2/issues/22506) -- Search suggestion panel overlapping on advance reporting button (fixed in [magento/magento2#22520](https://github.com/magento/magento2/pull/22520)) + * [#22869](https://github.com/magento/magento2/issues/22869) -- REST: Updating a customer without store_id sets the store_id to default (fixed in [magento/magento2#22893](https://github.com/magento/magento2/pull/22893)) + * [#22924](https://github.com/magento/magento2/issues/22924) -- Store view label not in the middle of panel (fixed in [magento/magento2#22926](https://github.com/magento/magento2/pull/22926)) + * [#20186](https://github.com/magento/magento2/issues/20186) -- phpcs error on rule classes - must be of the type integer (fixed in [magento/magento2#22947](https://github.com/magento/magento2/pull/22947)) + * [#574](https://github.com/magento/magento2/issues/574) -- Maximum function nesting level of '100' reached (fixed in [magento/graphql-ce#694](https://github.com/magento/graphql-ce/pull/694)) + * [#686](https://github.com/magento/magento2/issues/686) -- Product save validation errors in the admin don't hide the overlay (fixed in [magento/graphql-ce#695](https://github.com/magento/graphql-ce/pull/695)) + * [#22380](https://github.com/magento/magento2/issues/22380) -- Checkout totals order in specific store (fixed in [magento/magento2#22387](https://github.com/magento/magento2/pull/22387)) + * [#18183](https://github.com/magento/magento2/issues/18183) -- Magento 2.2.6 coupon codes don't work anymore (fixed in [magento/magento2#22718](https://github.com/magento/magento2/pull/22718)) + * [#22899](https://github.com/magento/magento2/issues/22899) -- Incorrect return type at getListByCustomerId in PaymentTokenManagementInterface (fixed in [magento/magento2#22914](https://github.com/magento/magento2/pull/22914)) + * [#22686](https://github.com/magento/magento2/issues/22686) -- Shipment Create via API salesShipmentRepositoryV1 throw Fatal error in Admin Order -> Shipment -> View (fixed in [magento/magento2#22687](https://github.com/magento/magento2/pull/22687)) + * [#22767](https://github.com/magento/magento2/issues/22767) -- Not clear logic for loading CMS Pages with setStoreId function (fixed in [magento/magento2#22772](https://github.com/magento/magento2/pull/22772)) + * [#20788](https://github.com/magento/magento2/issues/20788) -- Listing page no equal spacing in product in list view (fixed in [magento/magento2#22931](https://github.com/magento/magento2/pull/22931)) + * [#23030](https://github.com/magento/magento2/issues/23030) -- Magento2 Swatch change Image does not slide to first Image (fixed in [magento/magento2#23033](https://github.com/magento/magento2/pull/23033)) + * [#23034](https://github.com/magento/magento2/issues/23034) -- Wrong behaviour of validation scroll (fixed in [magento/magento2#23035](https://github.com/magento/magento2/pull/23035)) + * [#12696](https://github.com/magento/magento2/issues/12696) -- Integration tests create stub modules in app/code (fixed in [magento/magento2#18459](https://github.com/magento/magento2/pull/18459)) + * [#13266](https://github.com/magento/magento2/issues/13266) -- Topmenu 'last' class not being set if the a parent is inactive (fixed in [magento/magento2#22071](https://github.com/magento/magento2/pull/22071)) + * [#22882](https://github.com/magento/magento2/issues/22882) -- Static content deploy - Don't shows error message, just stack trace (fixed in [magento/magento2#22884](https://github.com/magento/magento2/pull/22884)) + * [#23045](https://github.com/magento/magento2/issues/23045) -- Exceptions from data patches do not show root cause (fixed in [magento/magento2#23046](https://github.com/magento/magento2/pull/23046)) + * [#16446](https://github.com/magento/magento2/issues/16446) -- magento 2.2.2 text swatch switches product image even if attribute feature is disabled (fixed in [magento/magento2#19184](https://github.com/magento/magento2/pull/19184)) + * [#14492](https://github.com/magento/magento2/issues/14492) -- Creating Customer without password is directly confirmed (fixed in [magento/magento2#21394](https://github.com/magento/magento2/pull/21394)) + * [#21671](https://github.com/magento/magento2/issues/21671) -- Database Media Storage - Transaction emails logo not used when pub/media cleared (fixed in [magento/magento2#21674](https://github.com/magento/magento2/pull/21674)) + * [#22425](https://github.com/magento/magento2/issues/22425) -- wrong url redirect when edit product review from Customer view page (fixed in [magento/magento2#22426](https://github.com/magento/magento2/pull/22426)) + * [#22511](https://github.com/magento/magento2/issues/22511) -- Special From Date set to today's date when Use Default Date checked in Store scope (fixed in [magento/magento2#22521](https://github.com/magento/magento2/pull/22521)) + * [#23080](https://github.com/magento/magento2/issues/23080) -- Missing whitespace in mobile navigation for non-English websites (fixed in [magento/magento2#23081](https://github.com/magento/magento2/pull/23081)) + * [#19872](https://github.com/magento/magento2/issues/19872) -- Magento 2.3 category edit page "Select from gallery" button not working. (fixed in [magento/magento2#21131](https://github.com/magento/magento2/pull/21131)) + * [#22092](https://github.com/magento/magento2/issues/22092) -- Assigning Catalog Image from Gallery, then Saving Twice, Clears Image (fixed in [magento/magento2#21131](https://github.com/magento/magento2/pull/21131)) + * [#22087](https://github.com/magento/magento2/issues/22087) -- Products Ordered Report - Not grouped by product (fixed in [magento/magento2#22646](https://github.com/magento/magento2/pull/22646)) + * [#21546](https://github.com/magento/magento2/issues/21546) -- [2.3] Database Media Storage - New Product Images fail to be processed correctly (fixed in [magento/magento2#21605](https://github.com/magento/magento2/pull/21605)) + * [#21604](https://github.com/magento/magento2/issues/21604) -- Database Media Storage - Admin Product Edit page does not handle product images correctly in database storage mode (fixed in [magento/magento2#21605](https://github.com/magento/magento2/pull/21605)) + * [#4247](https://github.com/magento/magento2/issues/4247) -- getProductUrl does not allow to override the scope in backend context (fixed in [magento/magento2#21876](https://github.com/magento/magento2/pull/21876)) + * [#22940](https://github.com/magento/magento2/issues/22940) -- Reset feature does not clear the date (fixed in [magento/magento2#23007](https://github.com/magento/magento2/pull/23007)) + * [#23053](https://github.com/magento/magento2/issues/23053) -- Sendfriend works for products with visibility not visible individually (fixed in [magento/magento2#23118](https://github.com/magento/magento2/pull/23118)) + * [#675](https://github.com/magento/magento2/issues/675) -- Textarea element cols and rows (fixed in [magento/graphql-ce#677](https://github.com/magento/graphql-ce/pull/677)) + * [#682](https://github.com/magento/magento2/issues/682) -- \Magento\Framework\Pricing\PriceCurrencyInterface depends on Magento application code (fixed in [magento/graphql-ce#700](https://github.com/magento/graphql-ce/pull/700)) + * [#681](https://github.com/magento/magento2/issues/681) -- Magento\Framework\Xml\Parser class issues (fixed in [magento/graphql-ce#711](https://github.com/magento/graphql-ce/pull/711)) + * [#22484](https://github.com/magento/magento2/issues/22484) -- Customer address States are duplicated in backend (fixed in [magento/magento2#22637](https://github.com/magento/magento2/pull/22637)) + * [#23138](https://github.com/magento/magento2/issues/23138) -- Magento_Theme. Incorrect configuration file location (fixed in [magento/magento2#23140](https://github.com/magento/magento2/pull/23140)) + * [#22004](https://github.com/magento/magento2/issues/22004) -- ce231 - can't update attribute for all product (fixed in [magento/magento2#22704](https://github.com/magento/magento2/pull/22704)) + * [#22870](https://github.com/magento/magento2/issues/22870) -- ProductRepository fails to update an existing product with a changed SKU (fixed in [magento/magento2#22933](https://github.com/magento/magento2/pull/22933)) + * [#22808](https://github.com/magento/magento2/issues/22808) -- php bin/magento catalog:image:resize error if image is missing (fixed in [magento/magento2#23005](https://github.com/magento/magento2/pull/23005)) + * [#674](https://github.com/magento/magento2/issues/674) -- Widgets in content pages. (fixed in [magento/graphql-ce#709](https://github.com/magento/graphql-ce/pull/709)) + * [#683](https://github.com/magento/magento2/issues/683) -- CMS Router not routing correctly (fixed in [magento/graphql-ce#717](https://github.com/magento/graphql-ce/pull/717)) + * [#9113](https://github.com/magento/magento2/issues/9113) -- [Bug or Feature?] url_path attribute value is not populated for any product (fixed in [magento/graphql-ce#721](https://github.com/magento/graphql-ce/pull/721)) + * [#18337](https://github.com/magento/magento2/issues/18337) -- #search input is missing required attribute aria-expanded. (fixed in [magento/magento2#22942](https://github.com/magento/magento2/pull/22942)) + * [#23213](https://github.com/magento/magento2/issues/23213) -- Static content deploy showing percentage(%) two times in progress bar (fixed in [magento/magento2#23216](https://github.com/magento/magento2/pull/23216)) + * [#23238](https://github.com/magento/magento2/issues/23238) -- Apply coupon button act like remove coupon while create new order from admin (fixed in [magento/magento2#23250](https://github.com/magento/magento2/pull/23250)) + * [#4788](https://github.com/magento/magento2/issues/4788) -- Wrong sitemap product url (fixed in [magento/magento2#23129](https://github.com/magento/magento2/pull/23129)) + * [#22934](https://github.com/magento/magento2/issues/22934) -- Incorrect work of "Use Categories Path for Product URLs" in sitemap generation. (fixed in [magento/magento2#23129](https://github.com/magento/magento2/pull/23129)) + * [#23266](https://github.com/magento/magento2/issues/23266) -- Cannot filter admin user by ID (fixed in [magento/magento2#23267](https://github.com/magento/magento2/pull/23267)) + * [#23285](https://github.com/magento/magento2/issues/23285) -- Credit memo submit button(refund) stays disable after validation fails & unable to enable button (fixed in [magento/magento2#23286](https://github.com/magento/magento2/pull/23286)) + * [#486](https://github.com/magento/magento2/issues/486) -- Take inspiration from other frameworks (fixed in [magento/graphql-ce#714](https://github.com/magento/graphql-ce/pull/714)) + * [#716](https://github.com/magento/magento2/issues/716) -- Wrong mimetype returned by getMimeType from Magento library (fixed in [magento/graphql-ce#723](https://github.com/magento/graphql-ce/pull/723)) + * [#687](https://github.com/magento/magento2/issues/687) -- Improvement Idea: /var/cache/mage--X (fixed in [magento/graphql-ce#749](https://github.com/magento/graphql-ce/pull/749)) + * [#20038](https://github.com/magento/magento2/issues/20038) -- loading icon disappearing before background process completes for braintree payment (Admin order) (fixed in [magento/magento2#22675](https://github.com/magento/magento2/pull/22675)) + * [#23074](https://github.com/magento/magento2/issues/23074) -- Magento 2.3.1 - URL rewrite rules are not creating for product after update url key (fixed in [magento/magento2#23309](https://github.com/magento/magento2/pull/23309)) + * [#622](https://github.com/magento/magento2/issues/622) -- FIX Magento Search Please! (fixed in [magento/graphql-ce#626](https://github.com/magento/graphql-ce/pull/626)) + * [#732](https://github.com/magento/magento2/issues/732) -- Inconsistency between Select and Multiselect form elements. (fixed in [magento/graphql-ce#734](https://github.com/magento/graphql-ce/pull/734)) + * [#13227](https://github.com/magento/magento2/issues/13227) -- Knockout Recently Viewed contains wrong product url (with category path), also not correct url on product view page (fixed in [magento/magento2#22650](https://github.com/magento/magento2/pull/22650)) + * [#22638](https://github.com/magento/magento2/issues/22638) -- Asterisk(*) sign position does not consistent in admin (fixed in [magento/magento2#22800](https://github.com/magento/magento2/pull/22800)) + * [#22266](https://github.com/magento/magento2/issues/22266) -- Product Alert after login shows 404 page (fixed in [magento/magento2#23218](https://github.com/magento/magento2/pull/23218)) + * [#23230](https://github.com/magento/magento2/issues/23230) -- Sticky header floating under top when there is no buttons in the toolbar (fixed in [magento/magento2#23247](https://github.com/magento/magento2/pull/23247)) + * [#23333](https://github.com/magento/magento2/issues/23333) -- Incorrect payment method translation in order emails (fixed in [magento/magento2#23338](https://github.com/magento/magento2/pull/23338)) + * [#23346](https://github.com/magento/magento2/issues/23346) -- 'Test Connection' button is over-spanned (fixed in [magento/magento2#23367](https://github.com/magento/magento2/pull/23367)) + * [#21380](https://github.com/magento/magento2/issues/21380) -- Cron schedule is being duplicated (fixed in [magento/magento2#23312](https://github.com/magento/magento2/pull/23312)) + * [#21136](https://github.com/magento/magento2/issues/21136) -- Magento installation via metapackage: checkExtensions fails (fixed in [magento/magento2#22116](https://github.com/magento/magento2/pull/22116)) + * [#23233](https://github.com/magento/magento2/issues/23233) -- Alert widget doesn't trigger always method on showing the message (fixed in [magento/magento2#23234](https://github.com/magento/magento2/pull/23234)) + * [#21974](https://github.com/magento/magento2/issues/21974) -- Changes for PayPal affect core config fields with tooltip (fixed in [magento/magento2#23393](https://github.com/magento/magento2/pull/23393)) + * [#23377](https://github.com/magento/magento2/issues/23377) -- Mini cart loader not working first time magento2 (fixed in [magento/magento2#23394](https://github.com/magento/magento2/pull/23394)) + * [#22998](https://github.com/magento/magento2/issues/22998) -- POST on /orders fails when properties in the body are out of sequence (fixed in [magento/magento2#23048](https://github.com/magento/magento2/pull/23048)) + * [#23522](https://github.com/magento/magento2/issues/23522) -- UPS shipping booking and label generation gives error when shipper's street given more than 35 chars (fixed in [magento/magento2#23523](https://github.com/magento/magento2/pull/23523)) + * [#8298](https://github.com/magento/magento2/issues/8298) -- Mobile Menu Behavior at Incorrect Breakpoint (fixed in [magento/magento2#23528](https://github.com/magento/magento2/pull/23528)) + * [#22103](https://github.com/magento/magento2/issues/22103) -- Character Encoding in Plain Text Emails Fails since 2.2.8/2.3.0 due to emails no longer being sent as MIME (fixed in [magento/magento2#23535](https://github.com/magento/magento2/pull/23535)) + * [#23199](https://github.com/magento/magento2/issues/23199) -- NO sender in email header for magento 2 sales order and password change emails to customer (fixed in [magento/magento2#23535](https://github.com/magento/magento2/pull/23535)) + * [#23538](https://github.com/magento/magento2/issues/23538) -- wrong validation happen for max-words validation class (fixed in [magento/magento2#23541](https://github.com/magento/magento2/pull/23541)) + * [#21126](https://github.com/magento/magento2/issues/21126) -- Backend Import behavior design break (fixed in [magento/magento2#21128](https://github.com/magento/magento2/pull/21128)) + * [#23471](https://github.com/magento/magento2/issues/23471) -- Tooltip missing at store view lable in Cms page and Cms block (fixed in [magento/magento2#23474](https://github.com/magento/magento2/pull/23474)) + * [#23466](https://github.com/magento/magento2/issues/23466) -- Cart empty after update qty with -1 and change address. (fixed in [magento/magento2#23477](https://github.com/magento/magento2/pull/23477)) + * [#23467](https://github.com/magento/magento2/issues/23467) -- Phone and Zip not update if customer have no saved address (fixed in [magento/magento2#23494](https://github.com/magento/magento2/pull/23494)) + * [#23222](https://github.com/magento/magento2/issues/23222) -- setup:upgrade should return failure when app:config:import failed (fixed in [magento/magento2#23310](https://github.com/magento/magento2/pull/23310)) + * [#23354](https://github.com/magento/magento2/issues/23354) -- Data saving problem error showing when leave blank qty and update it (fixed in [magento/magento2#23360](https://github.com/magento/magento2/pull/23360)) + * [#23424](https://github.com/magento/magento2/issues/23424) -- Search by keyword didn't work properly with "0" value (fixed in [magento/magento2#23427](https://github.com/magento/magento2/pull/23427)) + * [#16234](https://github.com/magento/magento2/issues/16234) -- Unable to enter `+` character in widget content (fixed in [magento/magento2#23496](https://github.com/magento/magento2/pull/23496)) + * [#9798](https://github.com/magento/magento2/issues/9798) -- Problem adding attribute options to configurable product via REST Api (fixed in [magento/magento2#23529](https://github.com/magento/magento2/pull/23529)) + * [#6287](https://github.com/magento/magento2/issues/6287) -- Customer Admin Shopping Cart View Missing (fixed in [magento/magento2#20918](https://github.com/magento/magento2/pull/20918)) + * [#22545](https://github.com/magento/magento2/issues/22545) -- Status downloadable product stays pending after succesfull payment (fixed in [magento/magento2#22658](https://github.com/magento/magento2/pull/22658)) + * [#23383](https://github.com/magento/magento2/issues/23383) -- Products which are not assigned to any store are automatically being force-assigned a store ID after being saved (fixed in [magento/magento2#23500](https://github.com/magento/magento2/pull/23500)) + * [#22950](https://github.com/magento/magento2/issues/22950) -- Spacing issue for Gift message section in my account (fixed in [magento/magento2#23226](https://github.com/magento/magento2/pull/23226)) + * [#23606](https://github.com/magento/magento2/issues/23606) -- Default value for report filters might result in errors (fixed in [magento/magento2#23607](https://github.com/magento/magento2/pull/23607)) + * [#736](https://github.com/magento/magento2/issues/736) -- Access to Zend Framework classes (fixed in [magento/graphql-ce#747](https://github.com/magento/graphql-ce/pull/747)) + * [#739](https://github.com/magento/magento2/issues/739) -- Command line install script no longer exists. (fixed in [magento/graphql-ce#753](https://github.com/magento/graphql-ce/pull/753)) + * [#23435](https://github.com/magento/magento2/issues/23435) -- Catalog Products Filter in 2.3.2 (fixed in [magento/magento2#23444](https://github.com/magento/magento2/pull/23444)) + * [#12817](https://github.com/magento/magento2/issues/12817) -- Coupon code with canceled order (fixed in [magento/magento2#20579](https://github.com/magento/magento2/pull/20579)) + * [#23386](https://github.com/magento/magento2/issues/23386) -- Copy Service does not works properly for Entities which extends Data Object and implements ExtensibleDataInterface (fixed in [magento/magento2#23387](https://github.com/magento/magento2/pull/23387)) + * [#23345](https://github.com/magento/magento2/issues/23345) -- Creditmemo getOrder() method loads order incorrectly (fixed in [magento/magento2#23358](https://github.com/magento/magento2/pull/23358)) + * [#22814](https://github.com/magento/magento2/issues/22814) -- Product stock alert - unsubscribe not working (fixed in [magento/magento2#23459](https://github.com/magento/magento2/pull/23459)) + * [#23594](https://github.com/magento/magento2/issues/23594) -- Database Media Storage : php bin/magento catalog:images:resize fails when image does not exist locally (fixed in [magento/magento2#23598](https://github.com/magento/magento2/pull/23598)) + * [#23595](https://github.com/magento/magento2/issues/23595) -- Database Media Storage : php bin/magento catalog:images:resize fails to generate cached images in database (fixed in [magento/magento2#23598](https://github.com/magento/magento2/pull/23598)) + * [#23596](https://github.com/magento/magento2/issues/23596) -- Database Media Storage : Add new product image, cached images not generated (fixed in [magento/magento2#23598](https://github.com/magento/magento2/pull/23598)) + * [#23643](https://github.com/magento/magento2/issues/23643) -- Mime parts of email are no more encoded with quoted printable (fixed in [magento/magento2#23649](https://github.com/magento/magento2/pull/23649)) + * [#23597](https://github.com/magento/magento2/issues/23597) -- Database Media Storage : Difficulty changing mode to database media storage due to poor "Use Default Value" checkbox behaviour (fixed in [magento/magento2#23710](https://github.com/magento/magento2/pull/23710)) + * [#23510](https://github.com/magento/magento2/issues/23510) -- Product customizable options of Area type render issue in Dashboard (fixed in [magento/magento2#23524](https://github.com/magento/magento2/pull/23524)) + * [#22890](https://github.com/magento/magento2/issues/22890) -- Disabled config can be overwritten via admin (fixed in [magento/magento2#22891](https://github.com/magento/magento2/pull/22891)) + * [#23054](https://github.com/magento/magento2/issues/23054) -- Cron job not running after crashed once (fixed in [magento/magento2#23125](https://github.com/magento/magento2/pull/23125)) + * [#23135](https://github.com/magento/magento2/issues/23135) -- Insert Variable popup missing template variables for new templates (fixed in [magento/magento2#23173](https://github.com/magento/magento2/pull/23173)) + * [#23211](https://github.com/magento/magento2/issues/23211) -- Zero Subtotal Checkout erroneously says the default value for "Automatically Invoice All Items" is "Yes" (fixed in [magento/magento2#23688](https://github.com/magento/magento2/pull/23688)) + * [#23624](https://github.com/magento/magento2/issues/23624) -- [Authorize.net accept.js] "Place Order" button not being disabled (fixed in [magento/magento2#23718](https://github.com/magento/magento2/pull/23718)) + * [#23717](https://github.com/magento/magento2/issues/23717) -- dependency injection fails for \Magento\ConfigurableProduct\Pricing\Price\PriceResolverInterface (fixed in [magento/magento2#23753](https://github.com/magento/magento2/pull/23753)) + * [#758](https://github.com/magento/magento2/issues/758) -- Coding standards: arrays (fixed in [magento/graphql-ce#759](https://github.com/magento/graphql-ce/pull/759)) + * [#14071](https://github.com/magento/magento2/issues/14071) -- Not able to change a position of last two related products in case of I've 20+ related products. (fixed in [magento/magento2#22984](https://github.com/magento/magento2/pull/22984)) + * [#22112](https://github.com/magento/magento2/issues/22112) -- Shipping address information is lost in billing step (fixed in [magento/magento2#23656](https://github.com/magento/magento2/pull/23656)) + * [#23654](https://github.com/magento/magento2/issues/23654) -- Frontend Label For Custom Order Status not Editable in Magento Admin in Single Store Mode (fixed in [magento/magento2#23681](https://github.com/magento/magento2/pull/23681)) + * [#23751](https://github.com/magento/magento2/issues/23751) -- Database Media Storage : PDF Logo file not database aware (fixed in [magento/magento2#23752](https://github.com/magento/magento2/pull/23752)) + * [#23678](https://github.com/magento/magento2/issues/23678) -- Can't see "Zero Subtotal Checkout" payment method settings if "Offline Payments" module is disabled (fixed in [magento/magento2#23679](https://github.com/magento/magento2/pull/23679)) + * [#23777](https://github.com/magento/magento2/issues/23777) -- "Discount Amount" field is validated after the page load without any action from user in Create New Catalog Rule form (fixed in [magento/magento2#23779](https://github.com/magento/magento2/pull/23779)) + * [#23789](https://github.com/magento/magento2/issues/23789) -- CommentLevelsSniff works incorrect with @magento_import statement. (fixed in [magento/magento2#23790](https://github.com/magento/magento2/pull/23790)) + * [#22702](https://github.com/magento/magento2/issues/22702) -- Toggle icon not working in create configuration Product creation Page (fixed in [magento/magento2#23803](https://github.com/magento/magento2/pull/23803)) + * [#167](https://github.com/magento/magento2/issues/167) -- Fatal error: Class 'Mage' not found (fixed in [magento/graphql-ce#351](https://github.com/magento/graphql-ce/pull/351)) + * [#438](https://github.com/magento/magento2/issues/438) -- [Feature request] Price slider (fixed in [magento/graphql-ce#699](https://github.com/magento/graphql-ce/pull/699)) + * [#702](https://github.com/magento/magento2/issues/702) -- Base table or view not found (fixed in [magento/graphql-ce#779](https://github.com/magento/graphql-ce/pull/779)) + * [#738](https://github.com/magento/magento2/issues/738) -- pub/setup missing in 0.1.0-alpha103 (fixed in [magento/graphql-ce#789](https://github.com/magento/graphql-ce/pull/789)) + * [#23405](https://github.com/magento/magento2/issues/23405) -- 2.3.2 installed and bin/magento setup:upgrade not working (fixed in [magento/magento2#23866](https://github.com/magento/magento2/pull/23866)) + * [#23900](https://github.com/magento/magento2/issues/23900) -- Report->Product->Downloads has wrong ACL (fixed in [magento/magento2#23901](https://github.com/magento/magento2/pull/23901)) + * [#23904](https://github.com/magento/magento2/issues/23904) -- No auto-focus after validation at "Create Configurations" button => User can not see the error message (fixed in [magento/magento2#23905](https://github.com/magento/magento2/pull/23905)) + * [#23916](https://github.com/magento/magento2/issues/23916) -- Missing Validation at some Payment Method Settings (fixed in [magento/magento2#23917](https://github.com/magento/magento2/pull/23917)) + * [#23932](https://github.com/magento/magento2/issues/23932) -- Decimal quantity is not displayed for wishlist items. (fixed in [magento/magento2#23933](https://github.com/magento/magento2/pull/23933)) +* GitHub pull requests: + * [magento/magento2#20135](https://github.com/magento/magento2/pull/20135) -- issue fixed #20124 Sort By label is hidden by Shop By Menu on listing (by @cedarvinda) + * [magento/magento2#22020](https://github.com/magento/magento2/pull/22020) -- Non existing file, when adding image to gallery with move option. Fix for #21978 (by @dudzio12) + * [magento/magento2#22260](https://github.com/magento/magento2/pull/22260) -- Disabling "Display on Product Details Page" the button is shown anyway. (by @Nazar65) + * [magento/magento2#22287](https://github.com/magento/magento2/pull/22287) -- #222249 configurable product images wrong sorting fix (by @Wirson) + * [magento/magento2#22526](https://github.com/magento/magento2/pull/22526) -- code cleanup (http to https) (by @ravi-chandra3197) + * [magento/magento2#22532](https://github.com/magento/magento2/pull/22532) -- fixed issue 22527 wishlist and compare icon alignment (by @sanjaychouhan-webkul) + * [magento/magento2#22423](https://github.com/magento/magento2/pull/22423) -- Store view specific labels cut in left navigation menu #22406 (by @sudhanshu-bajaj) + * [magento/magento2#22561](https://github.com/magento/magento2/pull/22561) -- [Catalog|Eav] Revert change of PR magento/magento2#13302 not included into revert commit (by @Den4ik) + * [magento/magento2#22569](https://github.com/magento/magento2/pull/22569) -- 2.3 develop pr1 (by @abhinay111222) + * [magento/magento2#22594](https://github.com/magento/magento2/pull/22594) -- Fixed typo issue (by @AfreenScarlet) + * [magento/magento2#22599](https://github.com/magento/magento2/pull/22599) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#22621](https://github.com/magento/magento2/pull/22621) -- Resolved Typo (by @UdgamN) + * [magento/magento2#19767](https://github.com/magento/magento2/pull/19767) -- Prevent display of token when save for later is not selected (by @pmclain) + * [magento/magento2#21744](https://github.com/magento/magento2/pull/21744) -- Custom option type select - Allow modify list of single selection option types (by @ihor-sviziev) + * [magento/magento2#21992](https://github.com/magento/magento2/pull/21992) -- #21473: Form element validation is not triggered when validation rules... (by @kisroman) + * [magento/magento2#22493](https://github.com/magento/magento2/pull/22493) -- Update credit-card-number-validator.js (by @justin-at-bounteous) + * [magento/magento2#22643](https://github.com/magento/magento2/pull/22643) -- Fixed typo issue and added missing header in customer sales order grid (by @vishal-7037) + * [magento/magento2#22656](https://github.com/magento/magento2/pull/22656) -- issues #22647 fixed, In customer account create page word not readable, should use '-' after break to new line In mobile view (by @cedarvinda) + * [magento/magento2#22720](https://github.com/magento/magento2/pull/22720) -- Fixed:#22395 (by @satyaprakashpatel) + * [magento/magento2#22558](https://github.com/magento/magento2/pull/22558) -- Additional condition in getRegion() method (by @Leone) + * [magento/magento2#22560](https://github.com/magento/magento2/pull/22560) -- Fix undefined methods 'addClass' and `removeClass` on a PrototypeJS Element (by @markvds) + * [magento/magento2#22606](https://github.com/magento/magento2/pull/22606) -- Fix Exception While Creating an Order in the Admin (by @justin-at-bounteous) + * [magento/magento2#22628](https://github.com/magento/magento2/pull/22628) -- Add a missting colon in the pdf page. (by @Hailong) + * [magento/magento2#22644](https://github.com/magento/magento2/pull/22644) -- Issue fixed #22636 arrow toggle not changing only showing to down It should be toggle as every where is working (by @cedarvinda) + * [magento/magento2#22664](https://github.com/magento/magento2/pull/22664) -- Fixed Typo Error (by @LuciferStrome) + * [magento/magento2#22729](https://github.com/magento/magento2/pull/22729) -- Fixed Typo Issue (by @jitendra-cedcoss) + * [magento/magento2#22734](https://github.com/magento/magento2/pull/22734) -- Ignores allure-results in git. (by @hostep) + * [magento/magento2#22758](https://github.com/magento/magento2/pull/22758) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#22798](https://github.com/magento/magento2/pull/22798) -- Disable Travis builds - 2.3-develop (by @okorshenko) + * [magento/magento2#22126](https://github.com/magento/magento2/pull/22126) -- Remove unnecessary form on order success page (by @danielgoodwin97) + * [magento/magento2#22655](https://github.com/magento/magento2/pull/22655) -- Fixed Issue #22640 (by @Surabhi-Cedcoss) + * [magento/magento2#22657](https://github.com/magento/magento2/pull/22657) -- 404 not found form validation url when updating quantity in cart page (by @gulshanchitransh) + * [magento/magento2#22739](https://github.com/magento/magento2/pull/22739) -- Revert "Magento backend catalog cost without currency symbol" as Cost... (by @orlangur) + * [magento/magento2#22779](https://github.com/magento/magento2/pull/22779) -- #22771 Remove hardcoded height for admin textarea (by @serhiyzhovnir) + * [magento/magento2#22791](https://github.com/magento/magento2/pull/22791) -- Fixed issue #22788 (by @gauravagarwal1001) + * [magento/magento2#19584](https://github.com/magento/magento2/pull/19584) -- Tierprice can t save float percentage value 18651 (by @novikor) + * [magento/magento2#21675](https://github.com/magento/magento2/pull/21675) -- [2.3] Database Media Storage - Design Config Save functions to be Database Media Storage aware (by @gwharton) + * [magento/magento2#21917](https://github.com/magento/magento2/pull/21917) -- Prevent duplicate variation error during import of configurable products with numerical SKUs (by @alexander-aleman) + * [magento/magento2#22463](https://github.com/magento/magento2/pull/22463) -- Set timezone on DateTime object not in constructor (by @NathMorgan) + * [magento/magento2#22575](https://github.com/magento/magento2/pull/22575) -- Fix for update products via csv file (fix for 22028) (by @mtwegrzycki) + * [magento/magento2#22794](https://github.com/magento/magento2/pull/22794) -- fixed issue #21558 - Navigation issue of review from product listing (by @sanjaychouhan-webkul) + * [magento/magento2#22844](https://github.com/magento/magento2/pull/22844) -- Allow to specify a field to be checked in the response (by @diazwatson) + * [magento/magento2#22186](https://github.com/magento/magento2/pull/22186) -- 22127: check that products is set (by @davidverholen) + * [magento/magento2#22418](https://github.com/magento/magento2/pull/22418) -- Patch the prototype pollution vulnerability in jQuery < 3.4.0 (CVE-2019-11358) (by @DanielRuf) + * [magento/magento2#22724](https://github.com/magento/magento2/pull/22724) -- Fixed issue #22639: Without select attribute click on add attribute it display all selected when add attribute again. (by @maheshWebkul721) + * [magento/magento2#22742](https://github.com/magento/magento2/pull/22742) -- issue #22676 fixed - Compare Products counter, and My Wish List count... (by @sanjaychouhan-webkul) + * [magento/magento2#22850](https://github.com/magento/magento2/pull/22850) -- only show customer account sections if payment method is active (by @torhoehn) + * [magento/magento2#22854](https://github.com/magento/magento2/pull/22854) -- fix #4628 font-face mixin add fromat option (by @Karlasa) + * [magento/magento2#22868](https://github.com/magento/magento2/pull/22868) -- fix date calculation for report's years interval (by @polkan-msk) + * [magento/magento2#21397](https://github.com/magento/magento2/pull/21397) -- Fixed Validation messages missing from datepicker form elements (by @ravi-chandra3197) + * [magento/magento2#22694](https://github.com/magento/magento2/pull/22694) -- Fixed Issue #20234 (by @surbhi-ranosys) + * [magento/magento2#22787](https://github.com/magento/magento2/pull/22787) -- #22786 Add dependency for UPS required fields to avoid validation for these fields if UPS Shipping is not active (by @serhiyzhovnir) + * [magento/magento2#22823](https://github.com/magento/magento2/pull/22823) -- [Shipping] Adjusting the Contact Us Xpath (by @eduard13) + * [magento/magento2#22830](https://github.com/magento/magento2/pull/22830) -- Removing "if" block and making code more legible (by @matheusgontijo) + * [magento/magento2#22839](https://github.com/magento/magento2/pull/22839) -- FIX: Add missing Stories and Severity to Test cases (by @lbajsarowicz) + * [magento/magento2#22858](https://github.com/magento/magento2/pull/22858) -- Grammatical mistake in the comments (by @sudhanshu-bajaj) + * [magento/magento2#22889](https://github.com/magento/magento2/pull/22889) -- Replace hardcoded CarierCode from createShippingMethod() (by @wigman) + * [magento/magento2#22922](https://github.com/magento/magento2/pull/22922) -- [BUGFIX] Set correct cron instance for catalog_product_frontend_actio... (by @lewisvoncken) + * [magento/magento2#22607](https://github.com/magento/magento2/pull/22607) -- Implement Better Error Handling and Fix Waits on Null PIDs in Parallel SCD Execution (by @davidalger) + * [magento/magento2#22795](https://github.com/magento/magento2/pull/22795) -- fixed issue #22736 - Cursor position not in right side of search keyword in mobile (by @sanjaychouhan-webkul) + * [magento/magento2#22876](https://github.com/magento/magento2/pull/22876) -- Fixed issue #22875 Billing Agreements page title need to be improved (by @amitvishvakarma) + * [magento/magento2#21215](https://github.com/magento/magento2/pull/21215) -- fixed-Discount-Code-improvement-21214 (by @abrarpathan19) + * [magento/magento2#22307](https://github.com/magento/magento2/pull/22307) -- Varnish health check failing due to presence of id_prefix in env.php (by @Nazar65) + * [magento/magento2#22444](https://github.com/magento/magento2/pull/22444) -- magento/magento2#22317: PR#22321 fix. (by @p-bystritsky) + * [magento/magento2#22513](https://github.com/magento/magento2/pull/22513) -- Fixed #22396 config:set fails with JSON values (by @shikhamis11) + * [magento/magento2#22520](https://github.com/magento/magento2/pull/22520) -- Fixed #22506: Search suggestion panel overlapping on advance reporting button (by @webkul-ajaysaini) + * [magento/magento2#22760](https://github.com/magento/magento2/pull/22760) -- [Forwardport] Magento Catalog - fix custom option type text price conversion for mu... (by @ihor-sviziev) + * [magento/magento2#22893](https://github.com/magento/magento2/pull/22893) -- #22869 - defaulting customer storeId fix (by @Wirson) + * [magento/magento2#22926](https://github.com/magento/magento2/pull/22926) -- Store view label not in the middle of panel (by @speedy008) + * [magento/magento2#22947](https://github.com/magento/magento2/pull/22947) -- phpcs error on rule classes - must be of the type integer (by @Nazar65) + * [magento/magento2#22951](https://github.com/magento/magento2/pull/22951) -- Update the contributing.md to match the new beginners guide (by @dmanners) + * [magento/magento2#22387](https://github.com/magento/magento2/pull/22387) -- Checkout totals order in specific store (by @krnshah) + * [magento/magento2#22718](https://github.com/magento/magento2/pull/22718) -- Resolved issue coupon codes don't work anymore #18183 (by @this-adarsh) + * [magento/magento2#22914](https://github.com/magento/magento2/pull/22914) -- #22899 Fix the issue with Incorrect return type at getListByCustomerId in PaymentTokenManagementInterface (by @serhiyzhovnir) + * [magento/magento2#22687](https://github.com/magento/magento2/pull/22687) -- #22686 Shipment view fixed for Fatal error. (by @milindsingh) + * [magento/magento2#22772](https://github.com/magento/magento2/pull/22772) -- Fixed issue #22767: Not clear logic for loading CMS Pages with setStoreId function (by @maheshWebkul721) + * [magento/magento2#22931](https://github.com/magento/magento2/pull/22931) -- [Fixed-20788: Listing page no equal spacing in product in list view] (by @hitesh-wagento) + * [magento/magento2#22965](https://github.com/magento/magento2/pull/22965) -- Simplify if else catalog search full text data provider (by @sankalpshekhar) + * [magento/magento2#23011](https://github.com/magento/magento2/pull/23011) -- Fix typehint (by @amenk) + * [magento/magento2#22920](https://github.com/magento/magento2/pull/22920) -- Mview Indexers getList should return integer values of id's and not strings (by @mhodge13) + * [magento/magento2#23020](https://github.com/magento/magento2/pull/23020) -- Remove direct use of object manager (by @AnshuMishra17) + * [magento/magento2#23033](https://github.com/magento/magento2/pull/23033) -- Issue fix #23030: Swatch change Image does not slide to first Image (by @milindsingh) + * [magento/magento2#23035](https://github.com/magento/magento2/pull/23035) -- [Validator] Fix wrong behaviour of validation scroll (by @Den4ik) + * [magento/magento2#18459](https://github.com/magento/magento2/pull/18459) -- 12696 Delete all test modules after integration tests (by @avstudnitz) + * [magento/magento2#19897](https://github.com/magento/magento2/pull/19897) -- Re-enable PriceBox block caching (by @brucemead) + * [magento/magento2#21200](https://github.com/magento/magento2/pull/21200) -- [FEATURE] Don't load product collection in review observer (by @Den4ik) + * [magento/magento2#22071](https://github.com/magento/magento2/pull/22071) -- Make sure 'last' class is set on top menu (by @arnoudhgz) + * [magento/magento2#22821](https://github.com/magento/magento2/pull/22821) -- Customer Account Forgot Password page title fix (by @textarea) + * [magento/magento2#22884](https://github.com/magento/magento2/pull/22884) -- Show exception message during SCD failure (by @ihor-sviziev) + * [magento/magento2#22989](https://github.com/magento/magento2/pull/22989) -- Properly transliterate German Umlauts (by @amenk) + * [magento/magento2#23036](https://github.com/magento/magento2/pull/23036) -- [Framework] Reassign fields variable after converting to array (by @Den4ik) + * [magento/magento2#23040](https://github.com/magento/magento2/pull/23040) -- Don't create a new account-nav block - use existing instead. (by @vovayatsyuk) + * [magento/magento2#23046](https://github.com/magento/magento2/pull/23046) -- Add more descriptive exception when data patch fails to apply. (by @ashsmith) + * [magento/magento2#23067](https://github.com/magento/magento2/pull/23067) -- Create Security.md file to show on GitHub Security/Policy page (by @piotrekkaminski) + * [magento/magento2#14384](https://github.com/magento/magento2/pull/14384) -- Don't throw shipping method exception when creating quote with only virtual products in API (by @Maikel-Koek) + * [magento/magento2#19184](https://github.com/magento/magento2/pull/19184) -- Fixed magento text swatch switches product image even if attribute feature is disabled #16446 (by @ravi-chandra3197) + * [magento/magento2#21394](https://github.com/magento/magento2/pull/21394) -- [2.3]creating customer without password is directly confirmed 14492 (by @novikor) + * [magento/magento2#21674](https://github.com/magento/magento2/pull/21674) -- [2.3] Database Media Storage - Transactional Emails will now extract image from database in Database Media Storage mode (by @gwharton) + * [magento/magento2#22336](https://github.com/magento/magento2/pull/22336) -- fix clean_cache plugin flush mode (by @thomas-kl1) + * [magento/magento2#22426](https://github.com/magento/magento2/pull/22426) -- Fixed wrong url redirect when edit product review from Customer view page (by @ravi-chandra3197) + * [magento/magento2#22521](https://github.com/magento/magento2/pull/22521) -- Fixed 22511 (by @maheshWebkul721) + * [magento/magento2#22626](https://github.com/magento/magento2/pull/22626) -- resolved typo error (by @nehaguptacedcoss) + * [magento/magento2#22834](https://github.com/magento/magento2/pull/22834) -- #16445 - getRegionHtmlSelect does not have configuration - resolved (by @nikunjskd20) + * [magento/magento2#22937](https://github.com/magento/magento2/pull/22937) -- Mark Elasticsearch 6 support for synonyms (by @aapokiiso) + * [magento/magento2#23081](https://github.com/magento/magento2/pull/23081) -- Fix missing whitespace in mobile navigation for non-English websites (by @alexeya-ven) + * [magento/magento2#21131](https://github.com/magento/magento2/pull/21131) -- Fix Issue #19872 - checking if image is in media directory (by @Bartlomiejsz) + * [magento/magento2#22341](https://github.com/magento/magento2/pull/22341) -- Apply coupoun and scroll top to check. applied successfully or not (by @krnshah) + * [magento/magento2#22646](https://github.com/magento/magento2/pull/22646) -- Fixed Issue #22087 (by @Surabhi-Cedcoss) + * [magento/magento2#23025](https://github.com/magento/magento2/pull/23025) -- Re-enable XML as request and response types within the SwaggerUI (by @sweikenb) + * [magento/magento2#20848](https://github.com/magento/magento2/pull/20848) -- Partial docs fixes in Newsletter module (by @SikailoISM) + * [magento/magento2#21605](https://github.com/magento/magento2/pull/21605) -- [2.3] Database Media Storage - Admin Product Edit Page handles recreates images correctly when pub/media/catalog is cleared. (by @gwharton) + * [magento/magento2#21876](https://github.com/magento/magento2/pull/21876) -- $product->getUrlInStore() does not allow to override the scope in backend context (by @Nazar65) + * [magento/magento2#23007](https://github.com/magento/magento2/pull/23007) -- [Fixed] Reset feature does not clear the date (by @niravkrish) + * [magento/magento2#23118](https://github.com/magento/magento2/pull/23118) -- #23053 : sendfriend verifies product visibility instead of status (by @Wirson) + * [magento/magento2#22637](https://github.com/magento/magento2/pull/22637) -- Fixed #22484 Customer address States are duplicated in backend (by @shikhamis11) + * [magento/magento2#23140](https://github.com/magento/magento2/pull/23140) -- magento/magento2#23138: Magento_Theme. Incorrect configuration file location (by @atwixfirster) + * [magento/magento2#23179](https://github.com/magento/magento2/pull/23179) -- Fix for translation function (by @kkdg) + * [magento/magento2#22704](https://github.com/magento/magento2/pull/22704) -- Fixed #22004 can't update attribute for all product (by @shikhamis11) + * [magento/magento2#22933](https://github.com/magento/magento2/pull/22933) -- magento/magento2#22870: ProductRepository fails to update an existing product with a changed SKU. (by @p-bystritsky) + * [magento/magento2#23005](https://github.com/magento/magento2/pull/23005) -- Improve command catalog:images:resize (by @tdgroot) + * [magento/magento2#22942](https://github.com/magento/magento2/pull/22942) -- Fixed issue #18337 (by @geet07) + * [magento/magento2#23216](https://github.com/magento/magento2/pull/23216) -- Fixed #23213 Static content deploy showing percentage symbol two times in progress bar (by @amitvishvakarma) + * [magento/magento2#23244](https://github.com/magento/magento2/pull/23244) -- Update CONTRIBUTING.md (by @diazwatson) + * [magento/magento2#23248](https://github.com/magento/magento2/pull/23248) -- fix tooltip toggle selector typo (by @Karlasa) + * [magento/magento2#23250](https://github.com/magento/magento2/pull/23250) -- Fixed #23238 Apply button act like remove button while create new order from admin (by @gauravagarwal1001) + * [magento/magento2#22211](https://github.com/magento/magento2/pull/22211) -- Show converted value for validateForRefund error message (by @kassner) + * [magento/magento2#23129](https://github.com/magento/magento2/pull/23129) -- #22934 Improved sitemap product generation logic (by @sergiy-v) + * [magento/magento2#23201](https://github.com/magento/magento2/pull/23201) -- MFTF: Use AdminLoginActionGroup for AdminLoginTest - easiest use of ActionGroup (by @lbajsarowicz) + * [magento/magento2#23100](https://github.com/magento/magento2/pull/23100) -- Resolve issue with improper EAV attribute join statement (by @udovicic) + * [magento/magento2#23267](https://github.com/magento/magento2/pull/23267) -- Add filter index for ID column in adminhtml user grid (by @JeroenVanLeusden) + * [magento/magento2#23286](https://github.com/magento/magento2/pull/23286) -- Fixed Credit memo submit button(refund) stays disable after validation fails & unable to enable button issue. (by @nishantjariwala) + * [magento/magento2#23292](https://github.com/magento/magento2/pull/23292) -- revert Properly transliterate German Umlauts (by @Nazar65) + * [magento/magento2#23307](https://github.com/magento/magento2/pull/23307) -- [Ui] Allow to define listing configuration via ui component xml (by @Den4ik) + * [magento/magento2#23335](https://github.com/magento/magento2/pull/23335) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#22675](https://github.com/magento/magento2/pull/22675) -- Fixed #20038 loading icon disappearing before background process completes for Braintree payment in Admin (by @kunal-rtpl) + * [magento/magento2#23174](https://github.com/magento/magento2/pull/23174) -- Move Quote related Plugins to correct module (by @sankalpshekhar) + * [magento/magento2#23309](https://github.com/magento/magento2/pull/23309) -- magento/magento2#23074: update correct product URL rewrites after changing category url key (by @sta1r) + * [magento/magento2#23347](https://github.com/magento/magento2/pull/23347) -- Fixes incorrect file reference in a comment in a .htaccess file. (by @hostep) + * [magento/magento2#22650](https://github.com/magento/magento2/pull/22650) -- Fixes issue #13227 (by @atishgoswami) + * [magento/magento2#22800](https://github.com/magento/magento2/pull/22800) -- fixed issue #22638 - Asterisk(*) sign position does not consistent in admin (by @sanjaychouhan-webkul) + * [magento/magento2#22910](https://github.com/magento/magento2/pull/22910) -- Do an empty check instead of isset check on image removed (by @arnoudhgz) + * [magento/magento2#23218](https://github.com/magento/magento2/pull/23218) -- Fixed #22266: 404 message for product alerts when not logged in (by @ArjenMiedema) + * [magento/magento2#23247](https://github.com/magento/magento2/pull/23247) -- Fixed 23230 : Sticky header floating under top when there is no buttons in the toolbar (by @konarshankar07) + * [magento/magento2#23338](https://github.com/magento/magento2/pull/23338) -- Fix issue with incorrect payment translation in sales emails (by @alexeya-ven) + * [magento/magento2#23366](https://github.com/magento/magento2/pull/23366) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#23367](https://github.com/magento/magento2/pull/23367) -- #23346: 'Test Connection' button is over-spanned (by @konarshankar07) + * [magento/magento2#22671](https://github.com/magento/magento2/pull/22671) -- Change exportButton option cvs (by @ajeetsinghcedcoss) + * [magento/magento2#23240](https://github.com/magento/magento2/pull/23240) -- Refactor: Improve mview code readability (by @lbajsarowicz) + * [magento/magento2#23280](https://github.com/magento/magento2/pull/23280) -- Ensure page is loaded after order click actions (by @fooman) + * [magento/magento2#23306](https://github.com/magento/magento2/pull/23306) -- FS/23038 Decimal qty with Increment is with specific values are not adding in cart (by @sertlab) + * [magento/magento2#23312](https://github.com/magento/magento2/pull/23312) -- Added function to check against running/pending/successful cron tasks (by @chickenland) + * [magento/magento2#22116](https://github.com/magento/magento2/pull/22116) -- Fix magento root package identification for metapackage installation (by @oleksii-lisovyi) + * [magento/magento2#23234](https://github.com/magento/magento2/pull/23234) -- [Ui] Calling the always action on opening and closing the modal. (by @eduard13) + * [magento/magento2#23353](https://github.com/magento/magento2/pull/23353) -- Get review entity id by code instead hard-coded. (by @DaniloEmpire) + * [magento/magento2#23393](https://github.com/magento/magento2/pull/23393) -- Fixed issue #21974 (by @geet07) + * [magento/magento2#23394](https://github.com/magento/magento2/pull/23394) -- Fixed issue #23377 (by @geet07) + * [magento/magento2#23403](https://github.com/magento/magento2/pull/23403) -- Remove rogue closing tag from store-switcher template (by @sta1r) + * [magento/magento2#22987](https://github.com/magento/magento2/pull/22987) -- Fixed apply discount coupons for bundle product (by @NikolasSumrak) + * [magento/magento2#23048](https://github.com/magento/magento2/pull/23048) -- #22998 : failing order creation with api when no address email is provided (by @Wirson) + * [magento/magento2#23390](https://github.com/magento/magento2/pull/23390) -- Changed logic so that _scrollToTopIfVisible is called only if element is in viewport. Previously it was called only when the element was outside it. (by @oskarolaussen) + * [magento/magento2#23425](https://github.com/magento/magento2/pull/23425) -- The best practices for SEO meta sequence. (by @vaseemishak) + * [magento/magento2#23523](https://github.com/magento/magento2/pull/23523) -- Issue #23522 UPS shipping booking and label generation gives error when shipper's street given more than 35 chars (by @ankurvr) + * [magento/magento2#23528](https://github.com/magento/magento2/pull/23528) -- move breakpoint by -1px to make nav work correctly at viewport 768 (by @bobemoe) + * [magento/magento2#23532](https://github.com/magento/magento2/pull/23532) -- Correct array type hints in Visibility model (by @pmclain) + * [magento/magento2#23535](https://github.com/magento/magento2/pull/23535) -- [2.3] Plain Text Emails are now sent with correct MIME Encoding (by @gwharton) + * [magento/magento2#23541](https://github.com/magento/magento2/pull/23541) -- fix validation class for max-words (by @sunilit42) + * [magento/magento2#21128](https://github.com/magento/magento2/pull/21128) -- Fix issue 21126 : Import design break issue resolved (by @speedy008) + * [magento/magento2#22213](https://github.com/magento/magento2/pull/22213) -- Date column ui component locale date format (by @Karlasa) + * [magento/magento2#23457](https://github.com/magento/magento2/pull/23457) -- Update CartTotalRepository.php (by @UlyanaKiklevich) + * [magento/magento2#23474](https://github.com/magento/magento2/pull/23474) -- Fixed tooltip missing at store view lable in Cms page and Cms block (by @dipeshrangani) + * [magento/magento2#23477](https://github.com/magento/magento2/pull/23477) -- Added quantity validation on Shipping Multiple Address Page (by @nirmalraval18) + * [magento/magento2#23494](https://github.com/magento/magento2/pull/23494) -- Removed editor from phone and zipcode (by @kazim-krish) + * [magento/magento2#23310](https://github.com/magento/magento2/pull/23310) -- magento/magento-2#23222: setup:upgrade should return failure when app... (by @ProcessEight) + * [magento/magento2#23360](https://github.com/magento/magento2/pull/23360) -- #23354 : Data saving problem error showing when leave blank qty and update it (by @konarshankar07) + * [magento/magento2#23427](https://github.com/magento/magento2/pull/23427) -- 23424: fixed search with 0 (by @jeysmook) + * [magento/magento2#23496](https://github.com/magento/magento2/pull/23496) -- Resolved + character issue in custom widget (by @sarfarazbheda) + * [magento/magento2#23529](https://github.com/magento/magento2/pull/23529) -- Feature/9798 updating configurable product options based on produc id and sku (by @lpouwelse) + * [magento/magento2#20918](https://github.com/magento/magento2/pull/20918) -- Enabled 'Shopping Cart' tab for customer edit interface in admin (by @rav-redchamps) + * [magento/magento2#22624](https://github.com/magento/magento2/pull/22624) -- Resolve Typo (by @prashantsharmacedcoss) + * [magento/magento2#22658](https://github.com/magento/magento2/pull/22658) -- Fixed #22545 Status downloadable product stays pending after succesfu... (by @shikhamis11) + * [magento/magento2#23500](https://github.com/magento/magento2/pull/23500) -- Fixed issue #23383 (by @manishgoswamij) + * [magento/magento2#23226](https://github.com/magento/magento2/pull/23226) -- Spacing issue for Gift message section in my account (by @amjadm61) + * [magento/magento2#23272](https://github.com/magento/magento2/pull/23272) -- hide or show the select for regions instead of enabling/disabling in customer registration (by @UB3RL33T) + * [magento/magento2#23593](https://github.com/magento/magento2/pull/23593) -- A small fix to improve format customer acl xml. (by @mrkhoa99) + * [magento/magento2#23607](https://github.com/magento/magento2/pull/23607) -- Default filter for reports set to past month (by @rogyar) + * [magento/magento2#22138](https://github.com/magento/magento2/pull/22138) -- BeanShells are changed to correct using of variables (by @AnnaShepa) + * [magento/magento2#22733](https://github.com/magento/magento2/pull/22733) -- Adds module:config:status command which checks if the module config i... (by @hostep) + * [magento/magento2#23351](https://github.com/magento/magento2/pull/23351) -- Fix some framework coding issues (by @fooman) + * [magento/magento2#23444](https://github.com/magento/magento2/pull/23444) -- Fix missing attribute_id condition from filter (by @mattijv) + * [magento/magento2#20579](https://github.com/magento/magento2/pull/20579) -- magento/magento2#12817: [Forwardport] Coupon code with canceled order. (by @p-bystritsky) + * [magento/magento2#23387](https://github.com/magento/magento2/pull/23387) -- magento/magento2#23386: Copy Service does not work properly for Entities which extends Data Object and implements ExtensibleDataInterface. (by @swnsma) + * [magento/magento2#23358](https://github.com/magento/magento2/pull/23358) -- magento/magento2#23345: Creditmemo getOrder() method loads order incorrectly. (by @p-bystritsky) + * [magento/magento2#23459](https://github.com/magento/magento2/pull/23459) -- magento/magento2#22814: Product stock alert - unsubscribe not working (by @yuriichayka) + * [magento/magento2#23598](https://github.com/magento/magento2/pull/23598) -- [2.3] magento catalog:images:resize now Database Media Storage mode aware (by @gwharton) + * [magento/magento2#23291](https://github.com/magento/magento2/pull/23291) -- Optimized dev:urn-catalog:generate for PHPStorm (by @JeroenBoersma) + * [magento/magento2#23592](https://github.com/magento/magento2/pull/23592) -- [Unit] Fix broken unit tests (by @Den4ik) + * [magento/magento2#23649](https://github.com/magento/magento2/pull/23649) -- [2.3] Transfer Encoding of emails changed to QUOTED-PRINTABLE (by @gwharton) + * [magento/magento2#23652](https://github.com/magento/magento2/pull/23652) -- Add missing getClass() to image.phtml so it is more like image_with_borders.phtml (by @woutersamaey) + * [magento/magento2#23710](https://github.com/magento/magento2/pull/23710) -- [2.3] Improve Database Media Storage Configuration settings usability (by @gwharton) + * [magento/magento2#23735](https://github.com/magento/magento2/pull/23735) -- Fixed typo in deploy module README.md (by @arnolds) + * [magento/magento2#22717](https://github.com/magento/magento2/pull/22717) -- Getting 404 url while updating quantity on multiple address cart page (by @vikalps4) + * [magento/magento2#23166](https://github.com/magento/magento2/pull/23166) -- Fix 22085 (by @geet07) + * [magento/magento2#23524](https://github.com/magento/magento2/pull/23524) -- remove html tag from option html from order page (by @sunilit42) + * [magento/magento2#22891](https://github.com/magento/magento2/pull/22891) -- Check if setting is disabled on default scope (by @kassner) + * [magento/magento2#23099](https://github.com/magento/magento2/pull/23099) -- fix customer data race condition when bundling is enabled (by @davidverholen) + * [magento/magento2#23125](https://github.com/magento/magento2/pull/23125) -- Catch throwables in mview updating (by @QuentinFarizonAfrimarket) + * [magento/magento2#23173](https://github.com/magento/magento2/pull/23173) -- Fixed issue #23135: Insert Variable popup missing template variables for new templates (by @maheshWebkul721) + * [magento/magento2#23688](https://github.com/magento/magento2/pull/23688) -- Resolve "Automatically Invoice All Items" is "Yes" but no invoice is created (Zero Subtotal Checkout) (by @edenduong) + * [magento/magento2#23718](https://github.com/magento/magento2/pull/23718) -- Resolve [Authorize.net accept.js] "Place Order" button not being disabled when editing billing address (by @edenduong) + * [magento/magento2#23753](https://github.com/magento/magento2/pull/23753) -- Add Magento\ConfigurableProduct\Pricing\Price\PriceResolverInterface to di.xml in issue23717 (by @edenduong) + * [magento/magento2#22984](https://github.com/magento/magento2/pull/22984) -- magento/magento2#14071: Not able to change a position of last two rel... (by @m-a-x-i-m) + * [magento/magento2#23656](https://github.com/magento/magento2/pull/23656) -- Fixes issue 22112 (https://github.com/magento/magento2/issues/22112) ... (by @rsimmons07) + * [magento/magento2#23666](https://github.com/magento/magento2/pull/23666) -- magento/magento2#: Fix storeId param type in the EmailNotification::newAccount, EmailNotificationInterface::newAccount methods (by @atwixfirster) + * [magento/magento2#23681](https://github.com/magento/magento2/pull/23681) -- Resolve Frontend Label For Custom Order Status not Editable in Magento Admin in Single Store Mode (by @edenduong) + * [magento/magento2#23752](https://github.com/magento/magento2/pull/23752) -- [2.3] Database Media Storage : PDF Logo file now database aware (by @gwharton) + * [magento/magento2#23679](https://github.com/magento/magento2/pull/23679) -- Moved Zero Subtotal Checkout Payment Settings (by @textarea) + * [magento/magento2#23779](https://github.com/magento/magento2/pull/23779) -- Resolve "Discount Amount" field is validated after the page load without any action from user in Create New Catalog Rule form issue23777 (by @edenduong) + * [magento/magento2#23787](https://github.com/magento/magento2/pull/23787) -- Fix for PHP_CodeSniffer error after app:config:dump (by @dng-dev) + * [magento/magento2#23790](https://github.com/magento/magento2/pull/23790) -- magento/magento2#23789: CommentLevelsSniff works incorrect with @magento_import statement. (by @p-bystritsky) + * [magento/magento2#23794](https://github.com/magento/magento2/pull/23794) -- Remove duplicate declaration (by @gfernandes410) + * [magento/magento2#23803](https://github.com/magento/magento2/pull/23803) -- Resolve Toggle icon not working in create configuration Product creation Page issue 22702 (by @edenduong) + * [magento/magento2#23782](https://github.com/magento/magento2/pull/23782) -- Cleaning some code gaps (by @Stepa4man) + * [magento/magento2#23840](https://github.com/magento/magento2/pull/23840) -- Fix regular expression comment on function isNameValid() in ImageContentValidator.php (by @nimbus2300) + * [magento/magento2#23845](https://github.com/magento/magento2/pull/23845) -- Add custom added url key to decoded directive string in WYSIWYG editor (by @JeroenVanLeusden) + * [magento/magento2#23866](https://github.com/magento/magento2/pull/23866) -- additional check for correct version of sodium (by @matei) + * [magento/magento2#23901](https://github.com/magento/magento2/pull/23901) -- Resolve Report->Product->Downloads has wrong ACL issue 23900 (by @edenduong) + * [magento/magento2#23905](https://github.com/magento/magento2/pull/23905) -- Resolve No auto-focus after validation at "Create Configurations" button => User can not see the error message issue23904 (by @edenduong) + * [magento/magento2#23917](https://github.com/magento/magento2/pull/23917) -- Resolve Missing Validation at some Payment Method Settings issue 23916 (by @edenduong) + * [magento/magento2#23919](https://github.com/magento/magento2/pull/23919) -- class ApplyAttributesUpdate should use \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE instead of fixing "bundle" (by @edenduong) + * [magento/magento2#23933](https://github.com/magento/magento2/pull/23933) -- Fix display of decimal quantities for wishlist items (by @mfickers) + 2.3.2 ============= * GitHub issues: diff --git a/README.md b/README.md index ec8bcdb292ea7..91f2ed19fb453 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ [![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.svg)](https://crowdin.com/project/magento-2) -

Welcome

+ +## 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. ## Magento System Requirements @@ -9,12 +10,16 @@ Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a ## Install Magento -* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). +* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). + +## Learn More About GraphQL in Magento 2 + +* [GraphQL Developer Guide](https://devdocs.magento.com/guides/v2.3/graphql/index.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. -To learn about how to make a contribution, click [here][1]. +To learn about how to contribute, click [here][1]. To learn about issues, click [here][2]. To open an issue, click [here][3]. @@ -26,14 +31,14 @@ To suggest documentation improvements, click [here][4]. [4]: https://devdocs.magento.com

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. +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 to these Community Maintainers for their valuable contributions.

Top Contributors

-Magento is thankful for any contribution that can improve our code base, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform. +Magento is thankful for any contribution that can improve our codebase, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform. @@ -44,7 +49,7 @@ Please review the [Code Contributions guide](https://devdocs.magento.com/guides/ ## Reporting Security Issues -To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up your issue. +To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up on your issue. 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). @@ -60,7 +65,7 @@ Please see LICENSE_EE.txt for the full text of the MEE License or visit https:// ## 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). +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 a request at [engcom@adobe.com](mailto:engcom@adobe.com) or [self signup](https://opensource.magento.com/slack). We have channels for each project. These channels are recommended for new members: diff --git a/app/code/Magento/AdminAnalytics/Controller/Adminhtml/Config/DisableAdminUsage.php b/app/code/Magento/AdminAnalytics/Controller/Adminhtml/Config/DisableAdminUsage.php new file mode 100644 index 0000000000000..34a9ef4f75b98 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Controller/Adminhtml/Config/DisableAdminUsage.php @@ -0,0 +1,104 @@ +configFactory = $configFactory; + $this->productMetadata = $productMetadata; + $this->notificationLogger = $notificationLogger; + } + + /** + * Change the value of config/admin/usage/enabled + */ + private function disableAdminUsage() + { + $configModel = $this->configFactory->create(); + $configModel->setDataByPath('admin/usage/enabled', 0); + $configModel->save(); + } + + /** + * Log information about the last admin usage selection + * + * @return ResultInterface + */ + private function markUserNotified(): ResultInterface + { + $responseContent = [ + 'success' => $this->notificationLogger->log( + $this->productMetadata->getVersion() + ), + 'error_message' => '' + ]; + + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + return $resultJson->setData($responseContent); + } + + /** + * Log information about the last shown advertisement + * + * @return ResultInterface + */ + public function execute() + { + $this->disableAdminUsage(); + $this->markUserNotified(); + } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed(static::ADMIN_RESOURCE); + } +} diff --git a/app/code/Magento/AdminAnalytics/Controller/Adminhtml/Config/EnableAdminUsage.php b/app/code/Magento/AdminAnalytics/Controller/Adminhtml/Config/EnableAdminUsage.php new file mode 100644 index 0000000000000..f70dd57aa59d6 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Controller/Adminhtml/Config/EnableAdminUsage.php @@ -0,0 +1,102 @@ +configFactory = $configFactory; + $this->productMetadata = $productMetadata; + $this->notificationLogger = $notificationLogger; + } + + /** + * Change the value of config/admin/usage/enabled + */ + private function enableAdminUsage() + { + $configModel = $this->configFactory->create(); + $configModel->setDataByPath('admin/usage/enabled', 1); + $configModel->save(); + } + + /** + * Log information about the last user response + * + * @return ResultInterface + */ + private function markUserNotified(): ResultInterface + { + $responseContent = [ + 'success' => $this->notificationLogger->log( + $this->productMetadata->getVersion() + ), + 'error_message' => '' + ]; + + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + return $resultJson->setData($responseContent); + } + + /** + * Log information about the last shown advertisement + * + * @return \Magento\Framework\Controller\ResultInterface + */ + public function execute() + { + $this->enableAdminUsage(); + $this->markUserNotified(); + } + + /** + * @inheritDoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed(static::ADMIN_RESOURCE); + } +} diff --git a/app/code/Magento/AdminAnalytics/Model/Condition/CanViewNotification.php b/app/code/Magento/AdminAnalytics/Model/Condition/CanViewNotification.php new file mode 100644 index 0000000000000..222261d4abfb5 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Model/Condition/CanViewNotification.php @@ -0,0 +1,84 @@ +viewerLogger = $viewerLogger; + $this->cacheStorage = $cacheStorage; + } + + /** + * Validate if notification popup can be shown and set the notification flag + * + * @param array $arguments Attributes from element node. + * @inheritdoc + */ + public function isVisible(array $arguments): bool + { + $cacheKey = self::$cachePrefix; + $value = $this->cacheStorage->load($cacheKey); + if ($value !== 'log-exists') { + $logExists = $this->viewerLogger->checkLogExists(); + if ($logExists) { + $this->cacheStorage->save('log-exists', $cacheKey); + } + return !$logExists; + } + return false; + } + + /** + * Get condition name + * + * @return string + */ + public function getName(): string + { + return self::$conditionName; + } +} diff --git a/app/code/Magento/AdminAnalytics/Model/ResourceModel/Viewer/Logger.php b/app/code/Magento/AdminAnalytics/Model/ResourceModel/Viewer/Logger.php new file mode 100644 index 0000000000000..c66f31e3d3bcb --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Model/ResourceModel/Viewer/Logger.php @@ -0,0 +1,111 @@ +resource = $resource; + $this->logFactory = $logFactory; + } + + /** + * Save (insert new or update existing) log. + * + * @param string $lastViewVersion + * @return bool + */ + public function log(string $lastViewVersion): bool + { + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = $this->resource->getConnection(ResourceConnection::DEFAULT_CONNECTION); + $connection->insertOnDuplicate( + $this->resource->getTableName(self::LOG_TABLE_NAME), + [ + 'last_viewed_in_version' => $lastViewVersion, + ], + [ + 'last_viewed_in_version', + ] + ); + return true; + } + + /** + * Get log by the last view version. + * + * @return Log + */ + public function get(): Log + { + return $this->logFactory->create(['data' => $this->loadLatestLogData()]); + } + + /** + * Checks is log already exists. + * + * @return boolean + */ + public function checkLogExists(): bool + { + $data = $this->logFactory->create(['data' => $this->loadLatestLogData()]); + $lastViewedVersion = $data->getLastViewVersion(); + return isset($lastViewedVersion); + } + + /** + * Load release notification viewer log data by last view version + * + * @return array + */ + private function loadLatestLogData(): array + { + $connection = $this->resource->getConnection(); + $select = $connection->select() + ->from(['log_table' => $this->resource->getTableName(self::LOG_TABLE_NAME)]) + ->order('log_table.id desc') + ->limit(['count' => 1]); + + $data = $connection->fetchRow($select); + if (!$data) { + $data = []; + } + return $data; + } +} diff --git a/app/code/Magento/AdminAnalytics/Model/Viewer/Log.php b/app/code/Magento/AdminAnalytics/Model/Viewer/Log.php new file mode 100644 index 0000000000000..0c3b6b81ec811 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Model/Viewer/Log.php @@ -0,0 +1,36 @@ +getData('id'); + } + + /** + * Get last viewed product version + * + * @return string + */ + public function getLastViewVersion() : ?string + { + return $this->getData('last_viewed_in_version'); + } +} diff --git a/app/code/Magento/AdminAnalytics/README.md b/app/code/Magento/AdminAnalytics/README.md new file mode 100644 index 0000000000000..e905344031ad3 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/README.md @@ -0,0 +1 @@ +The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin. \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/CloseAllDialogBoxesActionGroup.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/CloseAllDialogBoxesActionGroup.xml new file mode 100644 index 0000000000000..4fd7fd17c57ee --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/CloseAllDialogBoxesActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml new file mode 100644 index 0000000000000..d9f5e5dbcb106 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml new file mode 100644 index 0000000000000..5cf7be8a6fe11 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/SelectAdminUsageSettingActionGroup.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/SelectAdminUsageSettingActionGroup.xml new file mode 100644 index 0000000000000..3b302fe5be18b --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/ActionGroup/SelectAdminUsageSettingActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminHeaderSection.xml new file mode 100644 index 0000000000000..cc9f495a60026 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminHeaderSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminUsageConfigSection.xml similarity index 60% rename from app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml rename to app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminUsageConfigSection.xml index bab9842caaa42..634ebf855d940 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminUsageConfigSection.xml @@ -8,7 +8,8 @@ -
- +
+ +
diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminUsageNotificationSection.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminUsageNotificationSection.xml new file mode 100644 index 0000000000000..8bd6263d35e38 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Section/AdminUsageNotificationSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml new file mode 100644 index 0000000000000..58bcacc190cff --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + <description value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-18192"/> + <group value="backend"/> + <group value="login"/> + </annotations> + + <!-- Logging in Magento admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/Test/Unit/Condition/CanViewNotificationTest.php b/app/code/Magento/AdminAnalytics/Test/Unit/Condition/CanViewNotificationTest.php new file mode 100644 index 0000000000000..7819f2f017a01 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Unit/Condition/CanViewNotificationTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AdminAnalytics\Test\Unit\Condition; + +use Magento\AdminAnalytics\Model\Condition\CanViewNotification; +use Magento\AdminAnalytics\Model\ResourceModel\Viewer\Logger; +use Magento\AdminAnalytics\Model\Viewer\Log; +use Magento\Framework\App\ProductMetadataInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\CacheInterface; + +/** + * Class CanViewNotificationTest + */ +class CanViewNotificationTest extends \PHPUnit\Framework\TestCase +{ + /** @var CanViewNotification */ + private $canViewNotification; + + /** @var Logger|\PHPUnit_Framework_MockObject_MockObject */ + private $viewerLoggerMock; + + /** @var ProductMetadataInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $productMetadataMock; + + /** @var Log|\PHPUnit_Framework_MockObject_MockObject */ + private $logMock; + + /** @var $cacheStorageMock \PHPUnit_Framework_MockObject_MockObject|CacheInterface */ + private $cacheStorageMock; + + public function setUp() + { + $this->cacheStorageMock = $this->getMockBuilder(CacheInterface::class) + ->getMockForAbstractClass(); + $this->logMock = $this->getMockBuilder(Log::class) + ->getMock(); + $this->viewerLoggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManager = new ObjectManager($this); + $this->canViewNotification = $objectManager->getObject( + CanViewNotification::class, + [ + 'viewerLogger' => $this->viewerLoggerMock, + 'productMetadata' => $this->productMetadataMock, + 'cacheStorage' => $this->cacheStorageMock, + ] + ); + } + + /** + * @param $expected + * @param $cacheResponse + * @param $logExists + * @dataProvider isVisibleProvider + */ + public function testIsVisibleLoadDataFromLog($expected, $cacheResponse, $logExists) + { + $this->cacheStorageMock->expects($this->once()) + ->method('load') + ->with('admin-usage-notification-popup') + ->willReturn($cacheResponse); + $this->viewerLoggerMock + ->method('checkLogExists') + ->willReturn($logExists); + $this->cacheStorageMock + ->method('save') + ->with('log-exists', 'admin-usage-notification-popup'); + $this->assertEquals($expected, $this->canViewNotification->isVisible([])); + } + + /** + * @return array + */ + public function isVisibleProvider() + { + return [ + [true, false, false], + [false, 'log-exists', true], + [false, false, true], + ]; + } +} diff --git a/app/code/Magento/AdminAnalytics/Ui/DataProvider/AdminUsageNotificationDataProvider.php b/app/code/Magento/AdminAnalytics/Ui/DataProvider/AdminUsageNotificationDataProvider.php new file mode 100644 index 0000000000000..961a5663730a2 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Ui/DataProvider/AdminUsageNotificationDataProvider.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\AdminAnalytics\Ui\DataProvider; + +use Magento\Ui\DataProvider\AbstractDataProvider; +use Magento\Framework\Api\Filter; + +/** + * Data Provider for the Admin usage UI component. + */ +class AdminUsageNotificationDataProvider extends AbstractDataProvider +{ + /** + * @inheritdoc + */ + public function getData() + { + return $this->data; + } + + /** + * @inheritdoc + */ + public function addFilter(Filter $filter) + { + return null; + } +} diff --git a/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php b/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php new file mode 100644 index 0000000000000..9b1accbe0c823 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\AdminAnalytics\ViewModel; + +use Magento\Framework\App\ProductMetadataInterface; +use Magento\Backend\Model\Auth\Session; +use Magento\Framework\App\State; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Gets user version and mode + */ +class Metadata implements ArgumentInterface +{ + /** + * @var State + */ + private $appState; + + /** + * @var Session + */ + private $authSession; + + /** + * @var ProductMetadataInterface + */ + private $productMetadata; + + /** + * @param ProductMetadataInterface $productMetadata + * @param Session $authSession + * @param State $appState + */ + public function __construct( + ProductMetadataInterface $productMetadata, + Session $authSession, + State $appState + ) { + $this->productMetadata = $productMetadata; + $this->authSession = $authSession; + $this->appState = $appState; + } + + /** + * Get product version + * + * @return string + */ + public function getMagentoVersion() :string + { + return $this->productMetadata->getVersion(); + } + + /** + * Get current user id (hash generated from email) + * + * @return string + */ + public function getCurrentUser() :string + { + return hash('sha512', 'ADMIN_USER' . $this->authSession->getUser()->getEmail()); + } + /** + * Get Magento mode that the user is using + * + * @return string + */ + public function getMode() :string + { + return $this->appState->getMode(); + } +} diff --git a/app/code/Magento/AdminAnalytics/ViewModel/Notification.php b/app/code/Magento/AdminAnalytics/ViewModel/Notification.php new file mode 100644 index 0000000000000..030e027b83fce --- /dev/null +++ b/app/code/Magento/AdminAnalytics/ViewModel/Notification.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AdminAnalytics\ViewModel; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\AdminAnalytics\Model\Condition\CanViewNotification as AdminAnalyticsNotification; +use Magento\ReleaseNotification\Model\Condition\CanViewNotification as ReleaseNotification; + +/** + * Control display of admin analytics and release notification modals + */ +class Notification implements ArgumentInterface +{ + /** + * @var AdminAnalyticsNotification + */ + private $canViewNotificationAnalytics; + + /** + * @var ReleaseNotification + */ + private $canViewNotificationRelease; + + /** + * @param AdminAnalyticsNotification $canViewNotificationAnalytics + * @param ReleaseNotification $canViewNotificationRelease + */ + public function __construct( + AdminAnalyticsNotification $canViewNotificationAnalytics, + ReleaseNotification $canViewNotificationRelease + ) { + $this->canViewNotificationAnalytics = $canViewNotificationAnalytics; + $this->canViewNotificationRelease = $canViewNotificationRelease; + } + + /** + * Determine if the analytics popup is visible + * + * @return bool + */ + public function isAnalyticsVisible(): bool + { + return $this->canViewNotificationAnalytics->isVisible([]); + } + + /** + * Determine if the release popup is visible + * + * @return bool + */ + public function isReleaseVisible(): bool + { + return $this->canViewNotificationRelease->isVisible([]); + } +} diff --git a/app/code/Magento/AdminAnalytics/composer.json b/app/code/Magento/AdminAnalytics/composer.json new file mode 100644 index 0000000000000..0b977a23ad3ca --- /dev/null +++ b/app/code/Magento/AdminAnalytics/composer.json @@ -0,0 +1,29 @@ +{ + "name": "magento/module-admin-analytics", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-ui": "*", + "magento/module-release-notification": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AdminAnalytics\\": "" + } + } +} + diff --git a/app/code/Magento/AdminAnalytics/etc/adminhtml/routes.xml b/app/code/Magento/AdminAnalytics/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..5b5f2b52210b0 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/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="adminAnalytics" frontName="adminAnalytics"> + <module name="Magento_AdminAnalytics" /> + </route> + </router> +</config> diff --git a/app/code/Magento/AdminAnalytics/etc/adminhtml/system.xml b/app/code/Magento/AdminAnalytics/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..d6867e74c4760 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/adminhtml/system.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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="admin"> + <group id="usage" translate="label" type="text" sortOrder="2000" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Admin Usage</label> + <field id="enabled" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable Admin Usage Tracking</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Allow Magento to track admin usage in order to improve the quality and user experience.</comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/AdminAnalytics/etc/config.xml b/app/code/Magento/AdminAnalytics/etc/config.xml new file mode 100644 index 0000000000000..ba683f13c11e3 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/config.xml @@ -0,0 +1,18 @@ +<?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> + <admin> + <usage> + <enabled> + 1 + </enabled> + </usage> + </admin> + </default> +</config> \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/etc/db_schema.xml b/app/code/Magento/AdminAnalytics/etc/db_schema.xml new file mode 100644 index 0000000000000..ef1a657dc8243 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/db_schema.xml @@ -0,0 +1,23 @@ +<?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="admin_analytics_usage_version_log" resource="default" engine="innodb" + comment="Admin Notification Viewer Log Table"> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Log ID"/> + <column xsi:type="varchar" name="last_viewed_in_version" nullable="false" length="50" + comment="Viewer last viewed on product version"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="id"/> + </constraint> + <constraint xsi:type="unique" referenceId="ADMIN_ANALYTICS_USAGE_VERSION_LOG_LAST_VIEWED_IN_VERSION"> + <column name="last_viewed_in_version"/> + </constraint> + </table> +</schema> diff --git a/app/code/Magento/AdminAnalytics/etc/db_schema_whitelist.json b/app/code/Magento/AdminAnalytics/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..626e3ec14bc90 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/db_schema_whitelist.json @@ -0,0 +1,12 @@ +{ + "admin_analytics_usage_version_log": { + "column": { + "id": true, + "last_viewed_in_version": true + }, + "constraint": { + "PRIMARY": true, + "ADMIN_ANALYTICS_USAGE_VERSION_LOG_LAST_VIEWED_IN_VERSION": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/etc/module.xml b/app/code/Magento/AdminAnalytics/etc/module.xml new file mode 100644 index 0000000000000..f0990b114e25f --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/module.xml @@ -0,0 +1,10 @@ +<?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_AdminAnalytics"/> +</config> diff --git a/app/code/Magento/AdminAnalytics/registration.php b/app/code/Magento/AdminAnalytics/registration.php new file mode 100644 index 0000000000000..65c9955d396a8 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/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_AdminAnalytics', __DIR__); diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/layout/adminhtml_dashboard_index.xml b/app/code/Magento/AdminAnalytics/view/adminhtml/layout/adminhtml_dashboard_index.xml new file mode 100644 index 0000000000000..3069db1ecc2bb --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/layout/adminhtml_dashboard_index.xml @@ -0,0 +1,22 @@ +<?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"> + <uiComponent name="admin_usage_notification"> + <visibilityCondition name="can_view_admin_usage_notification" className="Magento\AdminAnalytics\Model\Condition\CanViewNotification"/> + </uiComponent> + <block name="tracking_notification" as="tracking_notification" template="Magento_AdminAnalytics::notification.phtml"> + <arguments> + <argument name="notification" xsi:type="object">Magento\AdminAnalytics\ViewModel\Notification</argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml b/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml new file mode 100644 index 0000000000000..7e379a17c78d7 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml @@ -0,0 +1,21 @@ +<?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="header"> + <block name="tracking" as="tracking" template="Magento_AdminAnalytics::tracking.phtml" ifconfig="admin/usage/enabled"> + <arguments> + <argument name="tracking_url" xsi:type="string">//assets.adobedtm.com/launch-EN30eb7ffa064444f1b8b0368ef38fd3a9.min.js</argument> + <argument name="metadata" xsi:type="object">Magento\AdminAnalytics\ViewModel\Metadata</argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> + + diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/requirejs-config.js b/app/code/Magento/AdminAnalytics/view/adminhtml/requirejs-config.js new file mode 100644 index 0000000000000..1361210929789 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_ReleaseNotification/js/modal/component': { + 'Magento_AdminAnalytics/js/release-notification/modal/component-mixin': true + } + } + } +}; diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml new file mode 100644 index 0000000000000..4b1f971670184 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml @@ -0,0 +1,16 @@ + +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> + +<script> + define('analyticsPopupConfig', function () { + return { + analyticsVisible: <?= $block->getNotification()->isAnalyticsVisible() ? 1 : 0; ?>, + releaseVisible: <?= $block->getNotification()->isReleaseVisible() ? 1 : 0; ?>, + } + }); +</script> diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml new file mode 100644 index 0000000000000..0ea5c753c9337 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> + +<script src="<?= $block->escapeUrl($block->getTrackingUrl()) ?>" async></script> +<script> + var adminAnalyticsMetadata = { + "version": "<?= $block->escapeJs($block->getMetadata()->getMagentoVersion()) ?>", + "user": "<?= $block->escapeJs($block->getMetadata()->getCurrentUser()) ?>", + "mode": "<?= $block->escapeJs($block->getMetadata()->getMode()) ?>" + }; +</script> diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml b/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml new file mode 100644 index 0000000000000..fcd1c4ebbbcdf --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml @@ -0,0 +1,95 @@ +<?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">admin_usage_notification.admin_usage_notification_data_source</item> + </item> + <item name="label" xsi:type="string" translate="true">Admin Usage Notification</item> + <item name="template" xsi:type="string">templates/form/collapsible</item> + </argument> + <settings> + <namespace>admin_usage_notification</namespace> + <dataScope>data</dataScope> + <deps> + <dep>admin_usage_notification.admin_usage_notification_data_source</dep> + </deps> + </settings> + <dataSource name="admin_usage_notification_data_source"> + <argument name="dataProvider" xsi:type="configurableObject"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="data" xsi:type="array"> + <item name="enableLogAction" xsi:type="url" path="adminAnalytics/config/enableAdminUsage"/> + <item name="disableLogAction" xsi:type="url" path="adminAnalytics/config/disableAdminUsage"/> + + </item> + </item> + </argument> + </argument> + <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> + <dataProvider class="Magento\AdminAnalytics\Ui\DataProvider\AdminUsageNotificationDataProvider" name="admin_usage_notification_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <modal name="notification_modal_1" component="Magento_AdminAnalytics/js/modal/component"> + <settings> + <state>true</state> + <options> + <option name="modalClass" xsi:type="string">admin-usage-notification</option> + <option name="title" xsi:type="string" translate="true">Allow admin usage data collection</option> + <option name="autoOpen" xsi:type="boolean">true</option> + <option name="type" xsi:type="string">popup</option> + <option name="clickableOverlay" xsi:type="boolean">false</option> + <option name="responsive" xsi:type="boolean">true</option> + <option name="innerScroll" xsi:type="boolean">false</option> + <option name="buttons" xsi:type="array"> + <item name="0" xsi:type="array"> + <item name="text" xsi:type="string" translate="true">Don't Allow</item> + <item name="class" xsi:type="string">action-secondary</item> + <item name="actions" xsi:type="array"> + <item name="0" xsi:type="string">disableAdminUsage</item> + </item> + </item> + <item name="1" xsi:type="array"> + <item name="text" xsi:type="string" translate="true">Allow</item> + <item name="class" xsi:type="string">action-primary</item> + <item name="actions" xsi:type="array"> + <item name="0" xsi:type="string">enableAdminUsage</item> + </item> + </item> + </option> + </options> + </settings> + <fieldset name="notification_fieldset"> + <settings> + <label/> + </settings> + <container name="notification_text" 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="additionalClasses" xsi:type="string">release-notification-text</item> + <item name="text" xsi:type="string" translate="true"><![CDATA[ + <p>Help us improve Magento Admin by allowing us to collect usage data.</p> + <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin and related products and services.</p> + <p>You can learn more and opt out at any time by following the instructions in <a href="https://docs.magento.com/m2/ce/user_guide/stores/admin.html" target="_blank" tabindex="0">merchant documentation</a>.</p> +]]></item> + </item> + </argument> + </container> + </fieldset> + </modal> +</form> diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/web/js/modal/component.js b/app/code/Magento/AdminAnalytics/view/adminhtml/web/js/modal/component.js new file mode 100644 index 0000000000000..fac71870603c3 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/web/js/modal/component.js @@ -0,0 +1,187 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'jquery', + 'Magento_Ui/js/modal/modal-component', + 'uiRegistry', + 'analyticsPopupConfig' +], + function (_, $, Modal, registry, analyticsPopupConfig) { + 'use strict'; + + return Modal.extend( + { + defaults: { + imports: { + enableLogAction: '${ $.provider }:data.enableLogAction', + disableLogAction: '${ $.provider }:data.disableLogAction' + }, + options: {}, + notificationWindow: null + }, + + /** + * Initializes modal on opened function + */ + initModal: function () { + this.options.opened = this.onOpened.bind(this); + this._super(); + }, + + /** + * Configure ESC and TAB so user can't leave modal + * without selecting an option + * + * @returns {Object} Chainable. + */ + initModalEvents: function () { + this._super(); + //Don't allow ESC key to close modal + this.options.keyEventHandlers.escapeKey = this.handleEscKey.bind(this); + //Restrict tab action to the modal + this.options.keyEventHandlers.tabKey = this.handleTabKey.bind(this); + + return this; + }, + + /** + * Once the modal is opened it hides the X + */ + onOpened: function () { + $('.modal-header button.action-close').attr('disabled', true).hide(); + + this.focusableElements = $(this.rootSelector).find('a[href], button:enabled'); + this.firstFocusableElement = this.focusableElements[0]; + this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1]; + this.firstFocusableElement.focus(); + }, + + /** + * Changes admin usage setting to yes + */ + enableAdminUsage: function () { + var data = { + 'form_key': window.FORM_KEY + }; + + $.ajax( + { + type: 'POST', + url: this.enableLogAction, + data: data, + showLoader: true + } + ).done( + function (xhr) { + if (xhr.error) { + self.onError(xhr); + } + } + ).fail(this.onError); + this.openReleasePopup(); + this.closeModal(); + }, + + /** + * Changes admin usage setting to no + */ + disableAdminUsage: function () { + var data = { + 'form_key': window.FORM_KEY + }; + + $.ajax( + { + type: 'POST', + url: this.disableLogAction, + data: data, + showLoader: true + } + ).done( + function (xhr) { + if (xhr.error) { + self.onError(xhr); + } + } + ).fail(this.onError); + this.openReleasePopup(); + this.closeModal(); + }, + + /** + * Allows admin usage popup to be shown first and then new release notification + */ + openReleasePopup: function () { + var notificationModalSelector = 'release_notification.release_notification.notification_modal_1'; + + if (analyticsPopupConfig.releaseVisible) { + registry.get(notificationModalSelector).initializeContentAfterAnalytics(); + } + }, + + /** + * Handle Tab and Shift+Tab key event + * + * Keep the tab actions restricted to the popup modal + * so the user must select an option to dismiss the modal + */ + handleTabKey: function (event) { + var modal = this, + KEY_TAB = 9; + + /** + * Handle Shift+Tab to tab backwards + */ + function handleBackwardTab() { + if (document.activeElement === modal.firstFocusableElement || + document.activeElement === $(modal.rootSelector)[0] + ) { + event.preventDefault(); + modal.lastFocusableElement.focus(); + } + } + + /** + * Handle Tab forward + */ + function handleForwardTab() { + if (document.activeElement === modal.lastFocusableElement) { + event.preventDefault(); + modal.firstFocusableElement.focus(); + } + } + + switch (event.keyCode) { + case KEY_TAB: + if (modal.focusableElements.length === 1) { + event.preventDefault(); + break; + } + + if (event.shiftKey) { + handleBackwardTab(); + break; + } + handleForwardTab(); + break; + default: + break; + } + }, + + /** + * Handle Esc key + * + * Esc key should not close modal + */ + handleEscKey: function (event) { + event.preventDefault(); + } + } + ); + } +); diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/web/js/release-notification/modal/component-mixin.js b/app/code/Magento/AdminAnalytics/view/adminhtml/web/js/release-notification/modal/component-mixin.js new file mode 100644 index 0000000000000..ffecd031cbb43 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/web/js/release-notification/modal/component-mixin.js @@ -0,0 +1,39 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define(['jquery', 'analyticsPopupConfig'], function ($, analyticsPopupConfig) { + 'use strict'; + + var deferred = $.Deferred(), + + mixin = { + /** + * Initializes content only if its visible + */ + initializeContent: function () { + var initializeContent = this._super.bind(this); + + if (!analyticsPopupConfig.analyticsVisible) { + initializeContent(); + } else { + deferred.then(function () { + initializeContent(); + }); + } + }, + + /** + * Initializes release notification content after admin analytics + */ + initializeContentAfterAnalytics: function () { + deferred.resolve(); + } + }; + + return function (target) { + return target.extend(mixin); + }; +}); + diff --git a/app/code/Magento/AdminNotification/Test/Mftf/ActionGroup/AdminSystemMessagesActionGroup.xml b/app/code/Magento/AdminNotification/Test/Mftf/ActionGroup/AdminSystemMessagesActionGroup.xml new file mode 100644 index 0000000000000..a498c95c65a30 --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/ActionGroup/AdminSystemMessagesActionGroup.xml @@ -0,0 +1,23 @@ +<?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="AdminSystemMessagesWarningActionGroup"> + <annotations> + <description>Check warning system message exists.</description> + </annotations> + <arguments> + <argument name="message" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminSystemMessagesSection.systemMessagesDropdown}}" stepKey="waitMessagesDropdownAppears"/> + <conditionalClick selector="{{AdminSystemMessagesSection.systemMessagesDropdown}}" dependentSelector="{{AdminSystemMessagesSection.messagesBlock}}" visible="false" stepKey="openMessagesBlockIfCollapsed"/> + <see userInput="{{message}}" selector="{{AdminSystemMessagesSection.warning}}" stepKey="seeWarningMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml index 8a73968edb9a6..e3b2ea7e24c83 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml @@ -11,5 +11,9 @@ <section name="AdminSystemMessagesSection"> <element name="systemMessagesDropdown" type="button" selector="#system_messages .message-system-action-dropdown"/> <element name="actionMessageLog" type="button" selector="//*[contains(@class, 'message-system-summary')]/a[contains(text(), '{{textMessage}}')]" parameterized="true"/> + <element name="messagesBlock" type="block" selector="#system_messages div.message-system-collapsible"/> + <element name="success" type="text" selector="#system_messages div.message-success"/> + <element name="warning" type="text" selector="#system_messages div.message-warning"/> + <element name="notice" type="text" selector="#system_messages div.message-notice"/> </section> </sections> diff --git a/app/code/Magento/AdminNotification/etc/db_schema.xml b/app/code/Magento/AdminNotification/etc/db_schema.xml index 29d928ced2084..8849687611193 100644 --- a/app/code/Magento/AdminNotification/etc/db_schema.xml +++ b/app/code/Magento/AdminNotification/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="adminnotification_inbox" resource="default" engine="innodb" comment="Adminnotification Inbox"> <column xsi:type="int" name="notification_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Notification id"/> + comment="Notification ID"/> <column xsi:type="smallint" name="severity" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Problem type"/> <column xsi:type="timestamp" name="date_added" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" @@ -35,7 +35,7 @@ </index> </table> <table name="admin_system_messages" resource="default" engine="innodb" comment="Admin System Messages"> - <column xsi:type="varchar" name="identity" nullable="false" length="100" comment="Message id"/> + <column xsi:type="varchar" name="identity" nullable="false" length="100" comment="Message ID"/> <column xsi:type="smallint" name="severity" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Problem type"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index fda6ae9530135..39009e5c7b4e3 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -5,11 +5,11 @@ */ 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; use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\ImportExport\Model\Export; +use Magento\Store\Model\Store; /** * Export Advanced Pricing @@ -150,6 +150,8 @@ public function __construct( } /** + * Init type models + * * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ @@ -172,7 +174,9 @@ protected function initTypeModels() } if ($model->isSuitable()) { $this->_productTypeModels[$productTypeName] = $model; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_disabledAttrs = array_merge($this->_disabledAttrs, $model->getDisabledAttrs()); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_indexValueAttributes = array_merge( $this->_indexValueAttributes, $model->getIndexValueAttributes() @@ -197,6 +201,7 @@ protected function initTypeModels() public function export() { //Execution time may be very long + // phpcs:ignore Magento2.Functions.DiscouragedFunction set_time_limit(0); $writer = $this->getWriter(); @@ -211,6 +216,7 @@ public function export() if ($entityCollection->count() == 0) { break; } + $entityCollection->clear(); $exportData = $this->getExportData(); foreach ($exportData as $dataRow) { $writer->writeRow($dataRow); @@ -234,16 +240,6 @@ public function filterAttributeCollection(\Magento\Eav\Model\ResourceModel\Entit foreach ($collection as $attribute) { if (in_array($attribute->getAttributeCode(), $this->_disabledAttrs)) { - if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_SKIP])) { - if ($attribute->getAttributeCode() == ImportAdvancedPricing::COL_TIER_PRICE - && in_array( - $attribute->getId(), - $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_SKIP] - ) - ) { - $this->_passTierPrice = 1; - } - } $collection->removeItemByKey($attribute->getId()); } } @@ -363,6 +359,7 @@ private function prepareExportData( $linkedTierPricesData = []; foreach ($tierPricesData as $tierPriceData) { $sku = $productLinkIdToSkuMap[$tierPriceData['product_link_id']]; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $linkedTierPricesData[] = array_merge( $tierPriceData, [ImportAdvancedPricing::COL_SKU => $sku] @@ -471,7 +468,7 @@ private function fetchTierPrices(array $productIds): array 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, + 'product_link_id' => 'ap.' . $productEntityLinkField, ]; if ($exportFilter && array_key_exists('tier_price', $exportFilter)) { if (!empty($exportFilter['tier_price'][0])) { @@ -488,7 +485,7 @@ private function fetchTierPrices(array $productIds): array $selectFields ) ->where( - 'ap.'.$productEntityLinkField.' IN (?)', + 'ap.' . $productEntityLinkField . ' IN (?)', $productIds ); @@ -602,7 +599,7 @@ protected function _getWebsiteCode(int $websiteId): string } if ($storeName && $currencyCode) { - $code = $storeName.' ['.$currencyCode.']'; + $code = $storeName . ' [' . $currencyCode . ']'; } else { $code = $storeName; } diff --git a/app/code/Magento/AdvancedSearch/Test/Unit/Model/Recommendations/DataProviderTest.php b/app/code/Magento/AdvancedSearch/Test/Unit/Model/Recommendations/DataProviderTest.php new file mode 100644 index 0000000000000..c62c906914fd7 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Unit/Model/Recommendations/DataProviderTest.php @@ -0,0 +1,189 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AdvancedSearch\Test\Unit\Model\Recommendations; + +use Magento\AdvancedSearch\Model\Recommendations\DataProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\AdvancedSearch\Model\ResourceModel\Recommendations; +use Magento\AdvancedSearch\Model\ResourceModel\RecommendationsFactory; +use Magento\Search\Model\QueryResult; +use Magento\Search\Model\QueryResultFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\Layer as SearchLayer; +use Magento\Store\Model\ScopeInterface; +use Magento\Search\Model\QueryInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * Class \Magento\AdvancedSearch\Test\Unit\Model\Recommendations\DataProviderTest + */ +class DataProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DataProvider; + */ + private $model; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ScopeConfigInterface + */ + private $scopeConfigMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Resolver + */ + private $layerResolverMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|SearchLayer + */ + private $searchLayerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RecommendationsFactory + */ + private $recommendationsFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Recommendations + */ + private $recommendationsMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Resolver + */ + private $queryResultFactory; + + /** + * Set up test environment. + * + * @return void + */ + protected function setUp() + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->layerResolverMock = $this->getMockBuilder(Resolver::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMock(); + + $this->searchLayerMock = $this->createMock(SearchLayer::class); + + $this->layerResolverMock->expects($this->any()) + ->method('get') + ->will($this->returnValue($this->searchLayerMock)); + + $this->recommendationsFactoryMock = $this->getMockBuilder(RecommendationsFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->recommendationsMock = $this->createMock(Recommendations::class); + + $this->queryResultFactory = $this->getMockBuilder(QueryResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + DataProvider::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'layerResolver' => $this->layerResolverMock, + 'recommendationsFactory' => $this->recommendationsFactoryMock, + 'queryResultFactory' => $this->queryResultFactory + ] + ); + } + + /** + * Test testGetItems() when Search Recommendations disabled. + * + * @return void + */ + public function testGetItemsWhenDisabledSearchRecommendations() + { + $isEnabledSearchRecommendations = false; + + /** @var $queryInterfaceMock QueryInterface */ + $queryInterfaceMock = $this->createMock(QueryInterface::class); + + $this->scopeConfigMock->expects($this->any()) + ->method('isSetFlag') + ->with('catalog/search/search_recommendations_enabled', ScopeInterface::SCOPE_STORE) + ->willReturn($isEnabledSearchRecommendations); + + $result = $this->model->getItems($queryInterfaceMock); + $this->assertEquals([], $result); + } + + /** + * Test testGetItems() when Search Recommendations enabled. + * + * @return void + */ + public function testGetItemsWhenEnabledSearchRecommendations() + { + $storeId = 1; + $searchRecommendationsCountConfig = 2; + $isEnabledSearchRecommendations = true; + $queryText = 'test'; + + /** @var $queryInterfaceMock QueryInterface */ + $queryInterfaceMock = $this->createMock(QueryInterface::class); + $queryInterfaceMock->expects($this->any())->method('getQueryText')->willReturn($queryText); + + $this->scopeConfigMock->expects($this->any()) + ->method('isSetFlag') + ->with('catalog/search/search_recommendations_enabled', ScopeInterface::SCOPE_STORE) + ->willReturn($isEnabledSearchRecommendations); + + $this->scopeConfigMock->expects($this->any()) + ->method('getValue') + ->with('catalog/search/search_recommendations_count', ScopeInterface::SCOPE_STORE) + ->willReturn($searchRecommendationsCountConfig); + + $productCollectionMock = $this->createMock(ProductCollection::class); + $productCollectionMock->expects($this->any())->method('getStoreId')->willReturn($storeId); + + $this->searchLayerMock->expects($this->any())->method('getProductCollection') + ->willReturn($productCollectionMock); + + $this->recommendationsFactoryMock->expects($this->any())->method('create') + ->willReturn($this->recommendationsMock); + + $this->recommendationsMock->expects($this->any())->method('getRecommendationsByQuery') + ->with($queryText, ['store_id' => $storeId], $searchRecommendationsCountConfig) + ->willReturn( + [ + [ + 'query_text' => 'a', + 'num_results' => 3 + ], + [ + 'query_text' => 'b', + 'num_results' => 2 + ] + ] + ); + $queryResultMock = $this->createMock(QueryResult::class); + $this->queryResultFactory->expects($this->any())->method('create')->willReturn($queryResultMock); + + $result = $this->model->getItems($queryInterfaceMock); + $this->assertEquals(2, count($result)); + } +} diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema.xml b/app/code/Magento/AdvancedSearch/etc/db_schema.xml index 2dd8c68e2d5fd..bf85a23782095 100644 --- a/app/code/Magento/AdvancedSearch/etc/db_schema.xml +++ b/app/code/Magento/AdvancedSearch/etc/db_schema.xml @@ -9,11 +9,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="catalogsearch_recommendations" resource="default" engine="innodb" comment="Advanced Search Recommendations"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="int" name="query_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Query Id"/> + default="0" comment="Query ID"/> <column xsi:type="int" name="relation_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Relation Id"/> + default="0" comment="Relation ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> diff --git a/app/code/Magento/Analytics/Model/ExportDataHandler.php b/app/code/Magento/Analytics/Model/ExportDataHandler.php index dc17a548763eb..4dbc316d0901a 100644 --- a/app/code/Magento/Analytics/Model/ExportDataHandler.php +++ b/app/code/Magento/Analytics/Model/ExportDataHandler.php @@ -157,7 +157,9 @@ private function prepareDirectory(WriteInterface $directory, $path) private function prepareFileDirectory(WriteInterface $directory, $path) { $directory->delete($path); + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (dirname($path) !== '.') { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $directory->create(dirname($path)); } @@ -176,6 +178,7 @@ private function pack($source, $destination) $this->archive->pack( $source, $destination, + // phpcs:ignore Magento2.Functions.DiscouragedFunction is_dir($source) ?: false ); diff --git a/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php b/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php index 4d62344197405..fecbf2033c1ba 100644 --- a/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php +++ b/app/code/Magento/Analytics/Model/ReportXml/ModuleIterator.php @@ -5,7 +5,7 @@ */ namespace Magento\Analytics\Model\ReportXml; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; /** * Iterator for ReportXml modules diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md index aa424182e2ebd..7ec30c6dd484b 100644 --- a/app/code/Magento/Analytics/README.md +++ b/app/code/Magento/Analytics/README.md @@ -1,18 +1,27 @@ -# Magento_Analytics Module +# 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](https://devdocs.magento.com/guides/v2.3/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) +- Enabling subscription to Magento Business Intelligence (MBI) and automatic re-subscription +- Declaring the configuration schemas for report data collection +- Collecting the Magento instance data as reports for 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) + +## Installation details + +Before disabling or uninstalling this module, note that the following modules depends on this module: +- Magento_CatalogAnalytics +- Magento_CustomerAnalytics +- Magento_QuoteAnalytics +- Magento_ReviewAnalytics +- Magento_SalesAnalytics +- Magento_WishlistAnalytics ## Structure @@ -29,12 +38,12 @@ The subscription to the MBI service is enabled during the installation process o 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 +- 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 diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml index 099cc71321b84..3b198644fcc9b 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Data/UserRoleData.xml @@ -9,6 +9,7 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="adminNoReportRole" type="user_role"> + <data key="all">0</data> <data key="rolename" unique="suffix">noreport</data> <data key="current_password">123123q</data> <array key="resource"> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index 58e62500b8203..8ebd8cb594bee 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -25,9 +25,9 @@ <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"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingHour}}" userInput="23" stepKey="selectAdvancedReportingHour"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingMinute}}" userInput="59" stepKey="selectAdvancedReportingMinute"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingSeconds}}" userInput="59" stepKey="selectAdvancedReportingSeconds"/> <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> </test> diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php index b08d41ac829b7..5288bcd306af9 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php @@ -7,7 +7,7 @@ namespace Magento\Analytics\Test\Unit\Model\ReportXml; use Magento\Analytics\Model\ReportXml\ModuleIterator; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml index c7da840b7e665..ad542cd30758d 100644 --- a/app/code/Magento/Analytics/etc/adminhtml/system.xml +++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml @@ -15,7 +15,7 @@ <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"> + "Go to Advanced Reporting" link. </br> For more information, see our <a target="_blank" 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> diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php index c37ae0d23dd25..6290ac00ce87f 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php @@ -87,22 +87,6 @@ public function getFailedOperationsByBulkId($bulkUuid, $failureType = null) */ 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) diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationSearchResults.php b/app/code/Magento/AsynchronousOperations/Model/OperationSearchResults.php new file mode 100644 index 0000000000000..84f0952a836c2 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationSearchResults.php @@ -0,0 +1,18 @@ +<?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\OperationSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with bulk Operation search result. + */ +class OperationSearchResults extends SearchResults implements OperationSearchResultsInterface +{ +} diff --git a/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml b/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml index 7190b80750357..e373a4fc78b13 100644 --- a/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml +++ b/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml @@ -13,6 +13,7 @@ <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> + <validate>validate-zero-or-greater validate-digits</validate> </field> </group> </section> diff --git a/app/code/Magento/AsynchronousOperations/etc/di.xml b/app/code/Magento/AsynchronousOperations/etc/di.xml index 42b62ff8ea374..94a4c56c19cea 100644 --- a/app/code/Magento/AsynchronousOperations/etc/di.xml +++ b/app/code/Magento/AsynchronousOperations/etc/di.xml @@ -16,7 +16,7 @@ <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\Data\OperationSearchResultsInterface" type="Magento\AsynchronousOperations\Model\OperationSearchResults" /> <preference for="Magento\AsynchronousOperations\Api\OperationRepositoryInterface" type="Magento\AsynchronousOperations\Model\OperationRepository" /> <type name="Magento\Framework\EntityManager\MetadataPool"> <arguments> diff --git a/app/code/Magento/Authorizenet/README.md b/app/code/Magento/Authorizenet/README.md index 380161d8b264e..62598837bee6d 100644 --- a/app/code/Magento/Authorizenet/README.md +++ b/app/code/Magento/Authorizenet/README.md @@ -1 +1,42 @@ +# Magento_Authorizenet module + The Magento_Authorizenet module implements the integration with the Authorize.Net payment gateway and makes the latter available as a payment method in Magento. + +## Extensibility + +Extension developers can interact with the Magento_Authorizenet module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Authorizenet module. + +### Events + +This module dispatches the following events: + + - `checkout_directpost_placeOrder` event in the `\Magento\Authorizenet\Controller\Directpost\Payment\Place::placeCheckoutOrder()` method. Parameters: + - `result` is a data object (`\Magento\Framework\DataObject` class). + - `action` is a controller object (`\Magento\Authorizenet\Controller\Directpost\Payment\Place`). + + - `order_cancel_after` event in the `\Magento\Authorizenet\Model\Directpost::declineOrder()` method. Parameters: + - `order` is an order object (`\Magento\Sales\Model\Order` class). + + +This module observes the following events: + + - `checkout_submit_all_after` event in the `Magento\Authorizenet\Observer\SaveOrderAfterSubmitObserver` file. + - `checkout_directpost_placeOrder` event in the `Magento\Authorizenet\Observer\AddFieldsToResponseObserver` file. + +For information about events in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `adminhtml_authorizenet_directpost_payment_redirect` + +This module introduces the following layouts and layout handles in the `view/frontend/layout` directory: + +- `authorizenet_directpost_payment_backendresponse` +- `authorizenet_directpost_payment_redirect` +- `authorizenet_directpost_payment_response` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). diff --git a/app/code/Magento/Authorizenet/Test/Mftf/Test/StorefrontVerifySecureURLRedirectAuthorizenetTest.xml b/app/code/Magento/Authorizenet/Test/Mftf/Test/StorefrontVerifySecureURLRedirectAuthorizenetTest.xml deleted file mode 100644 index 5db903f0ed54a..0000000000000 --- a/app/code/Magento/Authorizenet/Test/Mftf/Test/StorefrontVerifySecureURLRedirectAuthorizenetTest.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?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="StorefrontVerifySecureURLRedirectAuthorizenet"> - <annotations> - <features value="Authorizenet"/> - <stories value="Storefront Secure URLs"/> - <title value="Verify Secure URLs For Storefront Authorizenet Pages"/> - <description value="Verify that the Secure URL configuration applies to the Authorizenet pages on the Storefront"/> - <severity value="MAJOR"/> - <testCaseId value="MC-15610"/> - <group value="authorizenet"/> - <group value="configuration"/> - <group value="secure_storefront_url"/> - </annotations> - <before> - <createData entity="Simple_US_Customer" stepKey="customer"/> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> - <argument name="Customer" value="$$customer$$"/> - </actionGroup> - <executeJS function="return window.location.host" stepKey="hostname"/> - <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> - <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - </before> - <after> - <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <deleteData createDataKey="customer" stepKey="deleteCustomer"/> - </after> - <executeJS function="return window.location.host" stepKey="hostname"/> - <amOnUrl url="http://{$hostname}/authorizenet" stepKey="goToUnsecureAuthorizenetURL"/> - <seeCurrentUrlEquals url="https://{$hostname}/authorizenet" stepKey="seeSecureAuthorizenetURL"/> - </test> -</tests> diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php index 95c67f67852da..a1547a0563461 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php @@ -3,8 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Test\Unit\Model; +use Magento\Authorizenet\Helper\Backend\Data; +use Magento\Authorizenet\Helper\Data as HelperData; +use Magento\Authorizenet\Model\Directpost\Response; +use Magento\Authorizenet\Model\Directpost\Response\Factory as ResponseFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\ConfigInterface; use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Framework\Simplexml\Element; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -13,118 +24,118 @@ use Magento\Authorizenet\Model\Request; use Magento\Authorizenet\Model\Directpost\Request\Factory; use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; use Magento\Sales\Model\Order\Payment\Transaction\Repository as TransactionRepository; +use PHPUnit\Framework\MockObject_MockBuilder; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use ReflectionClass; /** * Class DirectpostTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class DirectpostTest extends \PHPUnit\Framework\TestCase +class DirectpostTest extends TestCase { const TOTAL_AMOUNT = 100.02; const INVOICE_NUM = '00000001'; const TRANSACTION_ID = '41a23x34fd124'; /** - * @var \Magento\Authorizenet\Model\Directpost + * @var Directpost */ protected $directpost; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|PHPUnit_Framework_MockObject_MockObject */ protected $scopeConfigMock; /** - * @var \Magento\Payment\Model\InfoInterface|\PHPUnit_Framework_MockObject_MockObject + * @var InfoInterface|PHPUnit_Framework_MockObject_MockObject */ protected $paymentMock; /** - * @var \Magento\Authorizenet\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var HelperData|PHPUnit_Framework_MockObject_MockObject */ protected $dataHelperMock; /** - * @var \Magento\Authorizenet\Model\Directpost\Response\Factory|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseFactory|PHPUnit_Framework_MockObject_MockObject */ protected $responseFactoryMock; /** - * @var TransactionRepository|\PHPUnit_Framework_MockObject_MockObject + * @var TransactionRepository|PHPUnit_Framework_MockObject_MockObject */ protected $transactionRepositoryMock; /** - * @var \Magento\Authorizenet\Model\Directpost\Response|\PHPUnit_Framework_MockObject_MockObject + * @var Response|PHPUnit_Framework_MockObject_MockObject */ protected $responseMock; /** - * @var TransactionService|\PHPUnit_Framework_MockObject_MockObject + * @var TransactionService|PHPUnit_Framework_MockObject_MockObject */ protected $transactionServiceMock; /** - * @var \Magento\Framework\HTTP\ZendClient|\PHPUnit_Framework_MockObject_MockObject + * @var ZendClient|PHPUnit_Framework_MockObject_MockObject */ protected $httpClientMock; /** - * @var \Magento\Authorizenet\Model\Directpost\Request\Factory|\PHPUnit_Framework_MockObject_MockObject + * @var Factory|PHPUnit_Framework_MockObject_MockObject */ protected $requestFactory; /** - * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentFailuresInterface|PHPUnit_Framework_MockObject_MockObject */ private $paymentFailures; + /** + * @var ZendClientFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $httpClientFactoryMock; + /** * @inheritdoc */ protected function setUp() { - $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->getMock(); - $this->paymentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getOrder', 'getId', 'setAdditionalInformation', 'getAdditionalInformation', - 'setIsTransactionDenied', 'setIsTransactionClosed', 'decrypt', 'getCcLast4', - 'getParentTransactionId', 'getPoNumber' - ]) - ->getMock(); - $this->dataHelperMock = $this->getMockBuilder(\Magento\Authorizenet\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - + $this->initPaymentMock(); $this->initResponseFactoryMock(); + $this->initHttpClientMock(); - $this->transactionRepositoryMock = $this->getMockBuilder( - \Magento\Sales\Model\Order\Payment\Transaction\Repository::class - ) + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)->getMock(); + $this->dataHelperMock = $this->getMockBuilder(HelperData::class)->disableOriginalConstructor()->getMock(); + $this->transactionRepositoryMock = $this->getMockBuilder(TransactionRepository::class) ->disableOriginalConstructor() ->setMethods(['getByTransactionId']) ->getMock(); - - $this->transactionServiceMock = $this->getMockBuilder(\Magento\Authorizenet\Model\TransactionService::class) + $this->transactionServiceMock = $this->getMockBuilder(TransactionService::class) ->disableOriginalConstructor() ->setMethods(['getTransactionDetails']) ->getMock(); - - $this->paymentFailures = $this->getMockBuilder( - PaymentFailuresInterface::class - ) + $this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class) ->disableOriginalConstructor() ->getMock(); - - $this->requestFactory = $this->getRequestFactoryMock(); - $httpClientFactoryMock = $this->getHttpClientFactoryMock(); + $this->requestFactory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->httpClientFactoryMock = $this->getMockBuilder(ZendClientFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); $helper = new ObjectManagerHelper($this); $this->directpost = $helper->getObject( - \Magento\Authorizenet\Model\Directpost::class, + Directpost::class, [ 'scopeConfig' => $this->scopeConfigMock, 'dataHelper' => $this->dataHelperMock, @@ -132,18 +143,97 @@ protected function setUp() 'responseFactory' => $this->responseFactoryMock, 'transactionRepository' => $this->transactionRepositoryMock, 'transactionService' => $this->transactionServiceMock, - 'httpClientFactory' => $httpClientFactoryMock, + 'httpClientFactory' => $this->httpClientFactoryMock, 'paymentFailures' => $this->paymentFailures, ] ); } + /** + * Create mock for response factory + * + * @return void + */ + private function initResponseFactoryMock() + { + $this->responseFactoryMock = $this->getMockBuilder(ResponseFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(Response::class) + ->setMethods( + [ + 'isValidHash', + 'getXTransId', + 'getXResponseCode', + 'getXResponseReasonCode', + 'getXResponseReasonText', + 'getXAmount', + 'setXResponseCode', + 'setXResponseReasonCode', + 'setXAvsCode', + 'setXResponseReasonText', + 'setXTransId', + 'setXInvoiceNum', + 'setXAmount', + 'setXMethod', + 'setXType', + 'setData', + 'getData', + 'setXAccountNumber', + '__wakeup' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->responseFactoryMock->expects($this->any())->method('create')->willReturn($this->responseMock); + } + + /** + * Create mock for payment + * + * @return void + */ + private function initPaymentMock() + { + $this->paymentMock = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getOrder', + 'setAmount', + 'setAnetTransType', + 'setXTransId', + 'getId', + 'setAdditionalInformation', + 'getAdditionalInformation', + 'setIsTransactionDenied', + 'setIsTransactionClosed', + 'decrypt', + 'getCcLast4', + 'getParentTransactionId', + 'getPoNumber' + ] + ) + ->getMock(); + } + + /** + * Create a mock for http client + * + * @return void + */ + private function initHttpClientMock() + { + $this->httpClientMock = $this->getMockBuilder(ZendClient::class) + ->disableOriginalConstructor() + ->setMethods(['request', 'getBody', '__wakeup']) + ->getMock(); + } + public function testGetConfigInterface() { - $this->assertInstanceOf( - \Magento\Payment\Model\Method\ConfigInterface::class, - $this->directpost->getConfigInterface() - ); + $this->assertInstanceOf(ConfigInterface::class, $this->directpost->getConfigInterface()); } public function testGetConfigValue() @@ -162,7 +252,7 @@ public function testSetDataHelper() $storeId = 'store-id'; $expectedResult = 'relay-url'; - $helperDataMock = $this->getMockBuilder(\Magento\Authorizenet\Helper\Backend\Data::class) + $helperDataMock = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); @@ -179,7 +269,7 @@ public function testAuthorize() { $paymentAction = 'some_action'; - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->once()) ->method('getValue') ->with('payment/authorizenet_directpost/payment_action', 'store', null) ->willReturn($paymentAction); @@ -190,11 +280,143 @@ public function testAuthorize() $this->directpost->authorize($this->paymentMock, 10); } + /** + * @dataProvider dataProviderCaptureWithInvalidAmount + * @expectedExceptionMessage Invalid amount for capture. + * @expectedException \Magento\Framework\Exception\LocalizedException + * + * @param int $invalidAmount + */ + public function testCaptureWithInvalidAmount($invalidAmount) + { + $this->directpost->capture($this->paymentMock, $invalidAmount); + } + + /** + * @return array + */ + public function dataProviderCaptureWithInvalidAmount() + { + return [ + [0], + [0.000], + [-1.000], + [-1], + [null], + ]; + } + + /** + * Test capture has parent transaction id. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testCaptureHasParentTransactionId() + { + $amount = 10; + + $this->paymentMock->expects($this->once())->method('setAmount')->with($amount); + $this->paymentMock->expects($this->exactly(2))->method('getParentTransactionId')->willReturn(1); + $this->paymentMock->expects($this->once())->method('setAnetTransType')->willReturn('PRIOR_AUTH_CAPTURE'); + + $this->paymentMock->expects($this->once())->method('getId')->willReturn(1); + $orderMock = $this->getOrderMock(); + $orderMock->expects($this->once())->method('getId')->willReturn(1); + $this->paymentMock->expects($this->once())->method('getOrder')->willReturn($orderMock); + + $transactionMock = $this->getMockBuilder(Transaction::class)->disableOriginalConstructor()->getMock(); + $this->transactionRepositoryMock->expects($this->once()) + ->method('getByTransactionId') + ->with(1, 1, 1) + ->willReturn($transactionMock); + + $this->paymentMock->expects($this->once())->method('setXTransId'); + $this->responseMock->expects($this->once())->method('getData')->willReturn([1]); + + $this->directpost->capture($this->paymentMock, 10); + } + + /** + * @@expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testCaptureWithoutParentTransactionId() + { + $amount = 10; + + $this->paymentMock->expects($this->once())->method('setAmount')->with($amount); + $this->paymentMock->expects($this->once())->method('getParentTransactionId')->willReturn(null); + $this->responseMock->expects($this->once())->method('getData')->willReturn([1]); + + $this->directpost->capture($this->paymentMock, 10); + } + + public function testCaptureWithoutParentTransactionIdWithoutData() + { + $amount = 10; + + $this->paymentMock->expects($this->once())->method('setAmount')->with($amount); + $this->paymentMock->expects($this->exactly(2))->method('getParentTransactionId')->willReturn(null); + $this->responseMock->expects($this->once())->method('getData')->willReturn([]); + + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(0) + ->willReturnSelf(); + + $this->httpClientFactoryMock->expects($this->once())->method('create')->willReturn($this->httpClientMock); + $this->httpClientMock->expects($this->once())->method('request')->willReturnSelf(); + + $this->buildRequestTest(); + $this->postRequestTest(); + + $this->directpost->capture($this->paymentMock, 10); + } + + private function buildRequestTest() + { + $orderMock = $this->getOrderMock(); + $orderMock->expects($this->once())->method('getStoreId')->willReturn(1); + $orderMock->expects($this->exactly(2))->method('getIncrementId')->willReturn(self::INVOICE_NUM); + $this->paymentMock->expects($this->once())->method('getOrder')->willReturn($orderMock); + + $this->addRequestMockToRequestFactoryMock(); + } + + private function postRequestTest() + { + $this->httpClientFactoryMock->expects($this->once())->method('create')->willReturn($this->httpClientMock); + $this->httpClientMock->expects($this->once())->method('request')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXResponseCode')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXResponseReasonCode')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXResponseReasonText')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXAvsCode')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXTransId')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXInvoiceNum')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXAmount')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXMethod')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXType')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setData')->willReturnSelf(); + + $response = $this->getRefundResponseBody( + Directpost::RESPONSE_CODE_APPROVED, + Directpost::RESPONSE_REASON_CODE_APPROVED, + 'Successful' + ); + $this->httpClientMock->expects($this->once())->method('getBody')->willReturn($response); + $this->responseMock->expects($this->once()) + ->method('getXResponseCode') + ->willReturn(Directpost::RESPONSE_CODE_APPROVED); + $this->responseMock->expects($this->once()) + ->method('getXResponseReasonCode') + ->willReturn(Directpost::RESPONSE_REASON_CODE_APPROVED); + $this->dataHelperMock->expects($this->never())->method('wrapGatewayError'); + } + public function testGetCgiUrl() { $url = 'cgi/url'; - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->once()) ->method('getValue') ->with('payment/authorizenet_directpost/cgi_url', 'store', null) ->willReturn($url); @@ -204,7 +426,7 @@ public function testGetCgiUrl() public function testGetCgiUrlWithEmptyConfigValue() { - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->once()) ->method('getValue') ->with('payment/authorizenet_directpost/cgi_url', 'store', null) ->willReturn(null); @@ -218,7 +440,7 @@ public function testGetRelayUrl() $url = 'relay/url'; $this->directpost->setData('store', $storeId); - $this->dataHelperMock->expects($this->any()) + $this->dataHelperMock->expects($this->exactly(2)) ->method('getRelayUrl') ->with($storeId) ->willReturn($url); @@ -268,7 +490,7 @@ public function testValidateResponseFailure() */ protected function prepareTestValidateResponse($transMd5, $login, $isValidHash) { - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->exactly(2)) ->method('getValue') ->willReturnMap( [ @@ -276,7 +498,7 @@ protected function prepareTestValidateResponse($transMd5, $login, $isValidHash) ['payment/authorizenet_directpost/login', 'store', null, $login] ] ); - $this->responseMock->expects($this->any()) + $this->responseMock->expects($this->exactly(1)) ->method('isValidHash') ->with($transMd5, $login) ->willReturn($isValidHash); @@ -328,6 +550,20 @@ public function checkResponseCodeSuccessDataProvider() ]; } + /** + * Checks response failures behaviour. + * + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testCheckResponseCodeFailureDefault() + { + $responseCode = 999999; + $this->responseMock->expects($this->once())->method('getXResponseCode')->willReturn($responseCode); + + $this->directpost->checkResponseCode(); + } + /** * Checks response failures behaviour. * @@ -338,34 +574,24 @@ public function checkResponseCodeSuccessDataProvider() * @expectedException \Magento\Framework\Exception\LocalizedException * @dataProvider checkResponseCodeFailureDataProvider */ - public function testCheckResponseCodeFailure(int $responseCode, int $failuresHandlerCalls): void + public function testCheckResponseCodeFailureDeclinedOrError(int $responseCode, int $failuresHandlerCalls): void { $reasonText = 'reason text'; $this->responseMock->expects($this->once()) ->method('getXResponseCode') ->willReturn($responseCode); - $this->responseMock->expects($this->any()) - ->method('getXResponseReasonText') - ->willReturn($reasonText); - $this->dataHelperMock->expects($this->any()) + $this->responseMock->expects($this->once())->method('getXResponseReasonText')->willReturn($reasonText); + $this->dataHelperMock->expects($this->once()) ->method('wrapGatewayError') ->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); + $this->paymentFailures->expects($this->exactly($failuresHandlerCalls))->method('handle')->with(1); + $orderMock = $this->getOrderMock($failuresHandlerCalls); - $reflection = new \ReflectionClass($this->directpost); + $orderMock->expects($this->exactly($failuresHandlerCalls))->method('getQuoteId')->willReturn(1); + $reflection = new ReflectionClass($this->directpost); $order = $reflection->getProperty('order'); $order->setAccessible(true); $order->setValue($this->directpost, $orderMock); @@ -381,7 +607,6 @@ public function checkResponseCodeFailureDataProvider(): array return [ ['responseCode' => Directpost::RESPONSE_CODE_DECLINED, 1], ['responseCode' => Directpost::RESPONSE_CODE_ERROR, 1], - ['responseCode' => 999999, 0], ]; } @@ -417,7 +642,7 @@ public function testCanCapture($isGatewayActionsLocked, $canCapture) { $this->directpost->setData('info_instance', $this->paymentMock); - $this->paymentMock->expects($this->any()) + $this->paymentMock->expects($this->once()) ->method('getAdditionalInformation') ->with(Directpost::GATEWAY_ACTIONS_LOCKED_STATE_KEY) ->willReturn($isGatewayActionsLocked); @@ -452,30 +677,16 @@ public function testFetchVoidedTransactionInfo($transactionId, $resultStatus, $r $paymentId = 36; $orderId = 36; - $this->paymentMock->expects(static::once()) - ->method('getId') - ->willReturn($paymentId); - - $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->setMethods(['getId', '__wakeup']) - ->getMock(); - $orderMock->expects(static::once()) - ->method('getId') - ->willReturn($orderId); + $this->paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); - $this->paymentMock->expects(static::once()) - ->method('getOrder') - ->willReturn($orderMock); - - $transactionMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment\Transaction::class) - ->disableOriginalConstructor() - ->getMock(); - $this->transactionRepositoryMock->expects(static::once()) + $orderMock = $this->getOrderMock(); + $orderMock->expects($this->once())->method('getId')->willReturn($orderId); + $this->paymentMock->expects($this->once())->method('getOrder')->willReturn($orderMock); + $transactionMock = $this->getMockBuilder(Transaction::class)->disableOriginalConstructor()->getMock(); + $this->transactionRepositoryMock->expects($this->once()) ->method('getByTransactionId') ->with($transactionId, $paymentId, $orderId) ->willReturn($transactionMock); - $document = $this->getTransactionXmlDocument( $transactionId, TransactionService::PAYMENT_UPDATE_STATUS_CODE_SUCCESS, @@ -483,20 +694,15 @@ public function testFetchVoidedTransactionInfo($transactionId, $resultStatus, $r $responseStatus, $responseCode ); - $this->transactionServiceMock->expects(static::once()) + $this->transactionServiceMock->expects($this->once()) ->method('getTransactionDetails') ->with($this->directpost, $transactionId) ->willReturn($document); // transaction should be closed - $this->paymentMock->expects(static::once()) - ->method('setIsTransactionDenied') - ->with(true); - $this->paymentMock->expects(static::once()) - ->method('setIsTransactionClosed') - ->with(true); - $transactionMock->expects(static::once()) - ->method('close'); + $this->paymentMock->expects($this->once())->method('setIsTransactionDenied')->with(true); + $this->paymentMock->expects($this->once())->method('setIsTransactionClosed')->with(true); + $transactionMock->expects($this->once())->method('close'); $this->directpost->fetchTransactionInfo($this->paymentMock, $transactionId); } @@ -509,60 +715,41 @@ public function testSuccessRefund() { $card = 1111; - $this->paymentMock->expects(static::exactly(2)) - ->method('getCcLast4') - ->willReturn($card); - $this->paymentMock->expects(static::once()) - ->method('decrypt') - ->willReturn($card); - $this->paymentMock->expects(static::exactly(3)) + $this->paymentMock->expects($this->exactly(1))->method('getCcLast4')->willReturn($card); + $this->paymentMock->expects($this->once())->method('decrypt')->willReturn($card); + $this->paymentMock->expects($this->exactly(3)) ->method('getParentTransactionId') ->willReturn(self::TRANSACTION_ID . '-capture'); - $this->paymentMock->expects(static::once()) - ->method('getPoNumber') - ->willReturn(self::INVOICE_NUM); - $this->paymentMock->expects(static::once()) + $this->paymentMock->expects($this->once())->method('getPoNumber')->willReturn(self::INVOICE_NUM); + $this->paymentMock->expects($this->once()) ->method('setIsTransactionClosed') ->with(true) ->willReturnSelf(); + $this->addRequestMockToRequestFactoryMock(); + $orderMock = $this->getOrderMock(); - $this->paymentMock->expects(static::exactly(2)) - ->method('getOrder') - ->willReturn($orderMock); + $orderMock->expects($this->once())->method('getId')->willReturn(1); + $orderMock->expects($this->exactly(2))->method('getIncrementId')->willReturn(self::INVOICE_NUM); + $orderMock->expects($this->once())->method('getStoreId')->willReturn(1); + + $this->paymentMock->expects($this->exactly(2))->method('getOrder')->willReturn($orderMock); - $transactionMock = $this->getMockBuilder(Order\Payment\Transaction::class) + $transactionMock = $this->getMockBuilder(Transaction::class) ->disableOriginalConstructor() ->setMethods(['getAdditionalInformation']) ->getMock(); - $transactionMock->expects(static::once()) + $transactionMock->expects($this->once()) ->method('getAdditionalInformation') ->with(Directpost::REAL_TRANSACTION_ID_KEY) ->willReturn(self::TRANSACTION_ID); - $this->transactionRepositoryMock->expects(static::once()) + $this->transactionRepositoryMock->expects($this->once()) ->method('getByTransactionId') ->willReturn($transactionMock); - $response = $this->getRefundResponseBody( - Directpost::RESPONSE_CODE_APPROVED, - Directpost::RESPONSE_REASON_CODE_APPROVED, - 'Successful' - ); - $this->httpClientMock->expects(static::once()) - ->method('getBody') - ->willReturn($response); - - $this->responseMock->expects(static::once()) - ->method('getXResponseCode') - ->willReturn(Directpost::RESPONSE_CODE_APPROVED); - $this->responseMock->expects(static::once()) - ->method('getXResponseReasonCode') - ->willReturn(Directpost::RESPONSE_REASON_CODE_APPROVED); - - $this->dataHelperMock->expects(static::never()) - ->method('wrapGatewayError'); + $this->postRequestTest(); $this->directpost->refund($this->paymentMock, self::TOTAL_AMOUNT); } @@ -583,65 +770,6 @@ public function dataProviderTransaction() ]; } - /** - * Create mock for response factory - * @return void - */ - private function initResponseFactoryMock() - { - $this->responseFactoryMock = $this->getMockBuilder( - \Magento\Authorizenet\Model\Directpost\Response\Factory::class - )->disableOriginalConstructor()->getMock(); - $this->responseMock = $this->getMockBuilder(\Magento\Authorizenet\Model\Directpost\Response::class) - ->setMethods( - [ - 'isValidHash', - 'getXTransId', 'getXResponseCode', 'getXResponseReasonCode', 'getXResponseReasonText', 'getXAmount', - 'setXResponseCode', 'setXResponseReasonCode', 'setXAvsCode', 'setXResponseReasonText', - 'setXTransId', 'setXInvoiceNum', 'setXAmount', 'setXMethod', 'setXType', 'setData', - 'setXAccountNumber', - '__wakeup' - ] - ) - ->disableOriginalConstructor() - ->getMock(); - - $this->responseMock->expects(static::any()) - ->method('setXResponseCode') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXResponseReasonCode') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXResponseReasonText') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXAvsCode') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXTransId') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXInvoiceNum') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXAmount') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXMethod') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXType') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setData') - ->willReturnSelf(); - - $this->responseFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->responseMock); - } - /** * Get transaction data * @param $transactionId @@ -694,80 +822,40 @@ private function getTransactionXmlDocument( /** * Get mock for authorize.net request factory - * @return \PHPUnit\Framework\MockObject_MockBuilder */ - private function getRequestFactoryMock() + private function addRequestMockToRequestFactoryMock() { - $requestFactory = $this->getMockBuilder(Factory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); $request = $this->getMockBuilder(Request::class) ->disableOriginalConstructor() ->setMethods(['__wakeup']) ->getMock(); - $requestFactory->expects(static::any()) + $this->requestFactory->expects($this->once()) ->method('create') ->willReturn($request); - return $requestFactory; } /** * Get mock for order - * @return \PHPUnit_Framework_MockObject_MockObject + * @return PHPUnit_Framework_MockObject_MockObject */ private function getOrderMock() { - $orderMock = $this->getMockBuilder(Order::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getId', 'getIncrementId', 'getStoreId', 'getBillingAddress', 'getShippingAddress', - 'getBaseCurrencyCode', 'getBaseTaxAmount', '__wakeup' - ]) - ->getMock(); - - $orderMock->expects(static::once()) - ->method('getId') - ->willReturn(1); - - $orderMock->expects(static::exactly(2)) - ->method('getIncrementId') - ->willReturn(self::INVOICE_NUM); - - $orderMock->expects(static::once()) - ->method('getStoreId') - ->willReturn(1); - - $orderMock->expects(static::once()) - ->method('getBaseCurrencyCode') - ->willReturn('USD'); - return $orderMock; - } - - /** - * Create and return mock for http client factory - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getHttpClientFactoryMock() - { - $this->httpClientMock = $this->getMockBuilder(\Magento\Framework\HTTP\ZendClient::class) + return $this->getMockBuilder(Order::class) ->disableOriginalConstructor() - ->setMethods(['request', 'getBody', '__wakeup']) - ->getMock(); - - $this->httpClientMock->expects(static::any()) - ->method('request') - ->willReturnSelf(); - - $httpClientFactoryMock = $this->getMockBuilder(\Magento\Framework\HTTP\ZendClientFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) + ->setMethods( + [ + 'getId', + 'getQuoteId', + 'getIncrementId', + 'getStoreId', + 'getBillingAddress', + 'getShippingAddress', + 'getBaseCurrencyCode', + 'getBaseTaxAmount', + '__wakeup' + ] + ) ->getMock(); - - $httpClientFactoryMock->expects(static::any()) - ->method('create') - ->willReturn($this->httpClientMock); - return $httpClientFactoryMock; } /** @@ -788,7 +876,9 @@ private function getRefundResponseBody($code, $reasonCode, $reasonText) $result[9] = self::TOTAL_AMOUNT; // XAmount $result[10] = Directpost::REQUEST_METHOD_CC; // XMethod $result[11] = Directpost::REQUEST_TYPE_CREDIT; // XType + // @codingStandardsIgnoreStart $result[37] = md5(self::TRANSACTION_ID); // x_MD5_Hash + // @codingStandardsIgnoreEnd $result[50] = '48329483921'; // setXAccountNumber return implode(Directpost::RESPONSE_DELIM_CHAR, $result); } diff --git a/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js b/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js index e43341ca2b337..eb162034bc04d 100644 --- a/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js +++ b/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js @@ -3,18 +3,11 @@ * See COPYING.txt for license details. */ -(function (factory) { - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'mage/backend/validation', - 'prototype' - ], factory); - } else { - factory(jQuery); - } -}(function (jQuery) { - +define([ + 'jquery', + 'mage/backend/validation', + 'prototype' +], function (jQuery) { window.directPost = Class.create(); directPost.prototype = { initialize: function (methodCode, iframeId, controller, orderSaveUrl, cgiUrl, nativeAction) { @@ -349,4 +342,4 @@ } } }; -})); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php index 9f10b2df40e9f..f669ead967c59 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php @@ -18,6 +18,8 @@ * Block for representing the payment form * * @api + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Form extends Cc { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php index ea476eaa55716..1876685998643 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php @@ -15,6 +15,8 @@ * Translates the labels for the info block * * @api + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Info extends ConfigurableInfo { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.php index 059bba0c805e8..b1c79e9e51426 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Payment.php @@ -18,6 +18,8 @@ * Represents the payment block for the admin checkout form * * @api + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Payment extends Template { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php index a72435644d23c..d59edde212760 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php @@ -14,6 +14,9 @@ /** * Chooses the best method of accepting the payment based on the status of the transaction + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AcceptPaymentStrategyCommand implements CommandInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php index a4d895d4daae0..4318441014ad7 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php @@ -20,6 +20,9 @@ /** * Chooses the best method of capture based on the context of the payment + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CaptureStrategyCommand implements CommandInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php index bb9e7c26a45b1..d0c1ceac81378 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php @@ -17,6 +17,9 @@ /** * Syncs the transaction status with authorize.net + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class FetchTransactionInfoCommand implements CommandInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php index f8975ef38eed1..7185639936fa4 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php @@ -20,6 +20,9 @@ /** * Makes a request to the gateway and returns results + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class GatewayQueryCommand implements CommandInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php index 3cdfcf23ba607..de3ded6515ae0 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php @@ -14,6 +14,9 @@ /** * Chooses the best method of returning the payment based on the status of the transaction + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class RefundTransactionStrategyCommand implements CommandInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php index 2a28945d98359..f41eb1660da55 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php @@ -13,6 +13,9 @@ /** * Houses configuration for this gateway + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Config extends \Magento\Payment\Gateway\Config\Config { @@ -33,19 +36,6 @@ class Config extends \Magento\Payment\Gateway\Config\Config private const SOLUTION_ID_SANDBOX = 'AAA102993'; private const SOLUTION_ID_PRODUCTION = 'AAA175350'; - /** - * @param ScopeConfigInterface $scopeConfig - * @param null|string $methodCode - * @param string $pathPattern - */ - public function __construct( - ScopeConfigInterface $scopeConfig, - $methodCode = null, - $pathPattern = self::DEFAULT_PATH_PATTERN - ) { - parent::__construct($scopeConfig, $methodCode, $pathPattern); - } - /** * Gets the login id * diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php index 1b2efbb85721a..ebd4240108a09 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php @@ -21,6 +21,9 @@ /** * A client that can communicate with the Authorize.net API + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Client implements ClientInterface { @@ -109,10 +112,12 @@ public function placeRequest(TransferInterface $transferObject) try { $data = $this->json->unserialize($responseBody); } catch (InvalidArgumentException $e) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Invalid JSON was returned by the gateway'); } return $data; + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { $this->logger->critical($e); diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php index a23397c09189a..cce878cfbbb16 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php @@ -12,6 +12,9 @@ /** * Removes a set of fields from the payload + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class RemoveFieldsFilter implements FilterInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php index 35e563eacb0cd..dade4bd4ee1f3 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php @@ -10,6 +10,9 @@ /** * Describes a filter for filtering content after all the builders have finished + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ interface FilterInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php index a4cdeba77492c..238b65fb8af37 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/TransferFactory.php @@ -14,6 +14,9 @@ /** * Can create a transfer object + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class TransferFactory implements TransferFactoryInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php index 6883d63397be0..4a673112e6a5f 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the meta transaction information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AcceptFdsDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php index e9c42e864440c..07a4921b7d60b 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php @@ -13,6 +13,9 @@ /** * Adds the basic payment information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AddressDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php index 601c329fe4f76..07fae5e536a28 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the amount of the transaction to the Request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AmountDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php index 2387ab0ab89f3..dec6626dc7524 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the stored credentials to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AuthenticationDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php index 226175f74d55a..c440da3ca9f4f 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php @@ -15,6 +15,9 @@ /** * Adds the meta transaction information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AuthorizeDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php index 0b17d10fb0d68..1e2a8617907a0 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php @@ -15,6 +15,9 @@ /** * Adds the meta transaction information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CaptureDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php index e5b4472c098c8..31246497fca92 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php @@ -14,6 +14,9 @@ /** * Adds the custom settings to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CustomSettingsBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php index 7cd0426e93dd7..cfdaa31552960 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the basic payment information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CustomerDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php index b0e33c9ca9615..bf0a15f552e6c 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php @@ -13,6 +13,9 @@ /** * Adds the basic payment information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class OrderDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php index 0301d08ad42c5..6e6ef04972c78 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php @@ -13,6 +13,9 @@ /** * Adds data to the request that can be used in the response + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PassthroughDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php index 1ad73f6236616..99955e9724577 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the basic payment information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PaymentDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php index ad8f8c2b05d91..9b56e0852af01 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the basic payment information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PoDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php index 96f3e67720fea..ac5bcb08cb04a 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the basic refund information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class RefundPaymentDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php index b8cb5f858d05d..65842354b7e2a 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the reference transaction to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class RefundReferenceTransactionDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php index 752be05f6b576..0f74299ebf5bd 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php @@ -12,6 +12,9 @@ /** * Adds the meta transaction information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class RefundTransactionTypeDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php index 16c3f9556de27..d20add70846b8 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php @@ -12,6 +12,9 @@ /** * Adds the type of the request to the build subject + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class RequestTypeBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php index 6ec27b105615b..4402fb5af8c82 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php @@ -15,6 +15,9 @@ /** * Adds the meta transaction information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class SaleDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php index 390714579f0b3..ea2cb89971fb5 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php @@ -15,6 +15,9 @@ /** * Adds the shipping information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class ShippingDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php index 0c89a0116defe..8734c0ab454ce 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the appropriate solution ID to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class SolutionDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php index f44b1e5de9a28..396ad143466cd 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php @@ -12,6 +12,9 @@ /** * This builder is used for correct store resolving and used only to retrieve correct store ID. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class StoreConfigBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php index 794c120f94451..a2766d97d9299 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StubDataBuilder.php @@ -15,6 +15,9 @@ * * Since the order of params is matters for Authorize.net request, * this builder is used to reserve a place in builders sequence. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class StubDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php index e3a17e9636846..9365347df7a60 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the reference transaction to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class TransactionDetailsDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php index ef0cb96774e62..c830f1f23d17c 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php @@ -14,6 +14,9 @@ /** * Adds the meta transaction information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class VoidDataBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php index 30b1ce88b083a..60c5bb21c0865 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php @@ -14,6 +14,9 @@ /** * Processes payment information from a void transaction response + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CloseParentTransactionHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php index fd8af3d28c4d4..5279df56b5e28 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/ClosePartialTransactionHandler.php @@ -11,6 +11,9 @@ /** * Determines that parent transaction should be close for partial refund operation. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class ClosePartialTransactionHandler extends CloseTransactionHandler { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php index fa9bf55462111..2cccf255ab8e9 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php @@ -14,6 +14,9 @@ /** * Processes payment information from a void transaction response + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CloseTransactionHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php index 16e8fbabb214a..e0b192205012f 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php @@ -14,6 +14,9 @@ /** * Processes payment information from a response + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PaymentResponseHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php index 9f7c62873669f..41c2ddd2b3271 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php @@ -14,6 +14,9 @@ /** * Processes payment information from a void transaction response + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PaymentReviewStatusHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php index 0dab641452136..81bb9c92b15ed 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php @@ -16,6 +16,9 @@ /** * Adds the details to the transaction that should show when the transaction is viewed in the admin + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class TransactionDetailsResponseHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php index bf5257f95dad6..f3a9a0a1c4466 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php @@ -14,6 +14,9 @@ /** * Processes transaction id for the payment + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class TransactionIdHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php index 06b16b37278ba..7bcb8c6c8dba1 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php @@ -14,6 +14,9 @@ /** * Processes payment information from a void transaction response + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class VoidResponseHandler implements HandlerInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php index 855d48e27968e..b5f1cef94ea46 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php @@ -13,6 +13,9 @@ /** * Helper for extracting information from the payment data structure + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class SubjectReader { @@ -42,6 +45,7 @@ public function readStoreId(array $subject): ?int $storeId = (int)$this->readPayment($subject) ->getOrder() ->getStoreId(); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\InvalidArgumentException $e) { // No store id is current set } diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php index 7ad4647b421a1..47065ed96c240 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php @@ -15,6 +15,9 @@ /** * Validates that the request was successful + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class GeneralResponseValidator extends AbstractValidator { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php index 0d1c2ad033d87..c11e22110d952 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php @@ -17,6 +17,9 @@ /** * Validates the transaction hash + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class TransactionHashValidator extends AbstractValidator { @@ -171,6 +174,7 @@ private function generateMd5Hash( $amount, $transactionId ) { + // phpcs:disable Magento2.Security.InsecureFunction return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); } diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php index 326f4fb29ac84..8238aa37dcc0a 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php @@ -15,6 +15,9 @@ /** * Validates the status of an attempted transaction + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class TransactionResponseValidator extends AbstractValidator { @@ -85,9 +88,7 @@ private function isResponseCodeAnError(array $transactionResponse): bool ?? $transactionResponse['errors'][0]['errorCode'] ?? null; - return !in_array($transactionResponse['responseCode'], [ - self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_HELD - ]) + return !in_array($transactionResponse['responseCode'], [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_HELD]) || $code && !in_array( $code, diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php index 046907ebb88cc..cdd1745a6bc1e 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php @@ -12,6 +12,9 @@ /** * Authorize.net Payment CC Types Source Model + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Cctype extends PaymentCctype { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php index f2eca8e143916..6f8e4394a589f 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Environment.php @@ -10,6 +10,9 @@ /** * Authorize.net Environment Dropdown source + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Environment implements \Magento\Framework\Data\OptionSourceInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php index 907a1b2a51b85..953841604bfee 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php @@ -10,6 +10,9 @@ /** * Authorize.net Payment Action Dropdown source + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PaymentAction implements \Magento\Framework\Data\OptionSourceInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php b/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php index b49ef7e622506..145d8c000e8f7 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php @@ -12,6 +12,9 @@ /** * Contains all the accumulated data from the request builders that should be passed through to the handlers + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class PassthroughDataObject extends DataObject { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php index b24c101a3f792..108f18f393641 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Ui/ConfigProvider.php @@ -14,6 +14,9 @@ /** * Retrieves config needed for checkout + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class ConfigProvider implements ConfigProviderInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php b/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php index c7490ad0c80c3..0f989bb032175 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php @@ -14,6 +14,9 @@ /** * Adds the payment info to the payment object + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class DataAssignObserver extends AbstractDataAssignObserver { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php b/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php index 0675bd94b6200..aa699569c61f6 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php @@ -18,6 +18,9 @@ /** * Copies the Authorize.net DirectPost configuration values to the new Accept.js module. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CopyCurrentConfig implements DataPatchInterface { diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml deleted file mode 100644 index 924f2b720dd2f..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml +++ /dev/null @@ -1,57 +0,0 @@ -<?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="ConfigureAuthorizenetAcceptjs" extends="EnableAuthorizenetAcceptjs"> - <annotations> - <description>Sets up the Authorize.net Accept JS configuration setting with a specified Payment action.</description> - </annotations> - <arguments> - <argument name="paymentAction" type="string"/> - </arguments> - <!-- Fill Auth.net fields and save --> - <waitForElementVisible selector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionCheckbox}}" stepKey="waitForFormVisible"/> - <conditionalClick selector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionCheckbox}}" stepKey="uncheckPaymentActionDefault" dependentSelector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionSelectDisabled}}" visible="true"/> - <selectOption selector="{{AuthorizenetAcceptjsConfigurationSection.paymentActionSelect}}" stepKey="selectPaymentAction" userInput="{{paymentAction}}"/> - <scrollTo selector="{{AuthorizenetAcceptjsConfigurationSection.apiLoginIdField}}" stepKey="scrollToApiLoginId"/> - <fillField selector="{{AuthorizenetAcceptjsConfigurationSection.apiLoginIdField}}" userInput="{{_CREDS.authorizenet_acceptjs_api_login_id}}" stepKey="fillApiLoginId"/> - <fillField selector="{{AuthorizenetAcceptjsConfigurationSection.transactionKeyField}}" userInput="{{_CREDS.authorizenet_acceptjs_transaction_key}}" stepKey="fillTransactionKey"/> - <fillField selector="{{AuthorizenetAcceptjsConfigurationSection.publicClientKeyField}}" userInput="{{_CREDS.authorizenet_acceptjs_public_client_key}}" stepKey="fillPublicClientKey"/> - <fillField selector="{{AuthorizenetAcceptjsConfigurationSection.signatureKeyField}}" userInput="{{_CREDS.authorizenet_acceptjs_signature_key}}" stepKey="fillSignatureKey"/> - </actionGroup> - - <actionGroup name="DisableAuthorizenetAcceptjs"> - <annotations> - <description>Disables the Authorize.net Accept JS configuration setting via the CLI.</description> - </annotations> - - <magentoCLI stepKey="disableAuthorizenetAcceptjs" command="config:set payment/authorizenet_acceptjs/active 0"/> - </actionGroup> - - <actionGroup name="EnableAuthorizenetAcceptjs"> - <scrollTo selector="{{AuthorizenetAcceptjsConfigurationSection.openSectionToggle}}" stepKey="scrollToAuthorizeNetConfigSection"/> - <conditionalClick selector="{{AuthorizenetAcceptjsConfigurationSection.openSectionToggle}}" dependentSelector="{{AuthorizenetAcceptjsConfigurationSection.enabledDefaultSelect}}" visible="false" stepKey="openConfigSection"/> - <waitForElementVisible selector="{{AuthorizenetAcceptjsConfigurationSection.enabledDefaultSelect}}" stepKey="waitForEnableFieldVisible"/> - <uncheckOption selector="{{AuthorizenetAcceptjsConfigurationSection.enabledDefaultCheckbox}}" stepKey="uncheckCheckbox"/> - <selectOption selector="{{AuthorizenetAcceptjsConfigurationSection.enabledDefaultSelect}}" userInput="Yes" stepKey="enablePayment"/> - </actionGroup> - - <actionGroup name="AssertAuthorizenetAcceptjsRequiredFieldsValidationIsPresentOnSave"> - <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> - <scrollTo selector="{{AuthorizenetAcceptjsConfigurationSection.apiLoginIdField}}" stepKey="scrollToApiLoginIdField"/> - <see selector="{{AuthorizenetAcceptjsConfigurationSection.apiLoginIdField}} + {{AdminConfigSection.fieldError}}" userInput="This is a required field." stepKey="seeApiLoginIdRequiredMessage"/> - <scrollTo selector="{{AuthorizenetAcceptjsConfigurationSection.publicClientKeyField}}" stepKey="scrollToPublicClientKeyField"/> - <see selector="{{AuthorizenetAcceptjsConfigurationSection.publicClientKeyField}} + {{AdminConfigSection.fieldError}}" userInput="This is a required field." stepKey="seePublicClientKeyRequiredErrorMessage"/> - <scrollTo selector="{{AuthorizenetAcceptjsConfigurationSection.transactionKeyField}}" stepKey="scrollTransactionKeyField"/> - <see selector="{{AuthorizenetAcceptjsConfigurationSection.transactionKeyField}} + {{AdminConfigSection.fieldError}}" userInput="This is a required field." stepKey="seeTransactionKeyRequiredErrorMessage"/> - <scrollTo selector="{{AuthorizenetAcceptjsConfigurationSection.signatureKeyField}}" stepKey="scrollToSignatureKeyField"/> - <see selector="{{AuthorizenetAcceptjsConfigurationSection.signatureKeyField}} + {{AdminConfigSection.fieldError}}" userInput="This is a required field." stepKey="seeSignatureKeyRequiredErrorMessage"/> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml deleted file mode 100644 index 96c0b122e36d9..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml +++ /dev/null @@ -1,46 +0,0 @@ -<?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="FillPaymentInformation"> - <annotations> - <description>Fill Payment Method with Authorize.net</description> - </annotations> - - <click stepKey="clickOnAuthorizenetToggle" selector="{{AuthorizenetCheckoutSection.selectAuthorizenet}}"/> - <waitForPageLoad stepKey="waitForCardDataSection"/> - <fillField stepKey="fillCardNumber" selector="{{AuthorizenetCheckoutSection.cardInput}}" userInput="{{PaymentAndShippingInfo.cardNumber}}"/> - <selectOption stepKey="fillExpMonth" selector="{{AuthorizenetCheckoutSection.expMonth}}" userInput="{{PaymentAndShippingInfo.month}}"/> - <selectOption stepKey="fillExpYear" selector="{{AuthorizenetCheckoutSection.expYear}}" userInput="20{{PaymentAndShippingInfo.year}}"/> - <fillField stepKey="fillCvv" selector="{{AuthorizenetCheckoutSection.cvv}}" userInput="123"/> - <click stepKey="checkoutButton" selector="{{AuthorizenetCheckoutSection.checkoutButton}}"/> - <waitForPageLoad stepKey="waitForCheckout"/> - </actionGroup> - - <!-- Guest checkout Authorize.net fill billing address --> - <actionGroup name="GuestCheckoutAuthorizenetFillBillingAddress"> - <annotations> - <description>Fill Billing Address as Guest with Authorize.net</description> - </annotations> - <arguments> - <argument name="customer"/> - <argument name="customerAddress"/> - </arguments> - - <fillField selector="{{GuestAuthorizenetCheckoutSection.firstName}}" userInput="{{customer.firstName}}" stepKey="fillFirstName"/> - <fillField selector="{{GuestAuthorizenetCheckoutSection.lastName}}" userInput="{{customer.lastName}}" stepKey="fillLastName"/> - <fillField selector="{{GuestAuthorizenetCheckoutSection.street}}" userInput="{{customerAddress.street[0]}}" stepKey="fillStreet"/> - <fillField selector="{{GuestAuthorizenetCheckoutSection.city}}" userInput="{{customerAddress.city}}" stepKey="fillCity"/> - <selectOption selector="{{GuestAuthorizenetCheckoutSection.state}}" userInput="{{customerAddress.state}}" stepKey="selectState"/> - <fillField selector="{{GuestAuthorizenetCheckoutSection.postcode}}" userInput="{{customerAddress.postcode}}" stepKey="fillPostCode"/> - <fillField selector="{{GuestAuthorizenetCheckoutSection.telephone}}" userInput="{{customerAddress.telephone}}" stepKey="fillTelephone"/> - <click selector="{{GuestAuthorizenetCheckoutSection.update}}" stepKey="updateAddress"/> - <waitForPageLoad stepKey="waitForUpdate"/> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml deleted file mode 100644 index ecbed57ff15b0..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml +++ /dev/null @@ -1,80 +0,0 @@ -<?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="ViewAndValidateOrderActionGroup"> - <annotations> - <description>Validate the Order is Correct. Then Submit the Invoice.</description> - </annotations> - <arguments> - <argument name="amount" type="string"/> - <argument name="status" type="string"/> - <argument name="captureStatus" type="string"/> - <argument name="closedStatus" type="string"/> - </arguments> - - <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> - <click selector="{{AdminMenuSection.sales}}" stepKey="clickSales"/> - <waitForPageLoad stepKey="waitForSalesSubsection"/> - <click selector="{{AdminMenuSection.orders}}" stepKey="clickOrders"/> - <waitForPageLoad stepKey="waitForOrdersGrid" time="30"/> - <click selector="{{OrdersGridSection.viewMostRecentOrder}}" stepKey="viewOrder"/> - <waitForPageLoad stepKey="waitForViewOrder"/> - <click selector="{{ViewOrderSection.openInvoiceForm}}" stepKey="openInvoiceForm"/> - <selectOption selector="{{ViewOrderSection.selectCaptureType}}" stepKey="selectCaptureType" userInput="Capture Online"/> - <click selector="{{ViewOrderSection.submitInvoice}}" stepKey="submitInvoice"/> - <waitForPageLoad stepKey="waitForInvoiceLoad"/> - <click selector="{{ViewOrderSection.commentsHistory}}" stepKey="viewCommentsHistory"/> - <waitForPageLoad stepKey="waitForHistoryLoad"/> - <see userInput="{{amount}}" selector="{{ViewOrderSection.capturedAmountText}}" stepKey="validateCapturedAmount"/> - <see userInput="{{status}}" selector="{{ViewOrderSection.orderStatus}}" stepKey="validateOrderStatus"/> - <click selector="{{ViewOrderSection.invoices}}" stepKey="openInvoices"/> - <waitForPageLoad stepKey="waitForInvoices"/> - <seeElement selector="{{ViewOrderSection.firstInvoice}}" stepKey="seeFirstInvoice"/> - <click selector="{{ViewOrderSection.transactions}}" stepKey="openTransactions"/> - <waitForPageLoad stepKey="waitForTransactions"/> - <see userInput="{{captureStatus}}" selector="{{ViewOrderSection.confirmCapture}}" stepKey="seeCapture"/> - <!-- Enable below line after fix of MC- - <see userInput="{{closedStatus}}" selector="{{ViewOrderSection.confirmClosed}}" stepKey="seeClosed"/> - --> - </actionGroup> - - <actionGroup name="ViewAndValidateOrderActionGroupNoSubmit"> - <annotations> - <description>Validate the Order is Correct. Do Not Submit the Invoice.</description> - </annotations> - <arguments> - <argument name="amount" type="string"/> - <argument name="status" type="string"/> - <argument name="captureStatus" type="string"/> - <argument name="closedStatus" type="string"/> - </arguments> - - <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> - <click selector="{{AdminMenuSection.sales}}" stepKey="clickSales"/> - <waitForPageLoad stepKey="waitForSalesSubsection"/> - <click selector="{{AdminMenuSection.orders}}" stepKey="clickOrders"/> - <waitForPageLoad stepKey="waitForOrdersGrid" time="30"/> - <click selector="{{OrdersGridSection.viewMostRecentOrder}}" stepKey="viewOrder"/> - <waitForPageLoad stepKey="waitForViewOrder"/> - <click selector="{{ViewOrderSection.commentsHistory}}" stepKey="viewCommentsHistory"/> - <waitForPageLoad stepKey="waitForHistoryLoad"/> - <see userInput="{{amount}}" selector="{{ViewOrderSection.capturedAmountTextUnsubmitted}}" stepKey="validateCapturedAmount"/> - <see userInput="{{status}}" selector="{{ViewOrderSection.orderStatus}}" stepKey="validateOrderStatus"/> - <click selector="{{ViewOrderSection.invoices}}" stepKey="openInvoices"/> - <waitForPageLoad stepKey="waitForInvoices"/> - <seeElement selector="{{ViewOrderSection.firstInvoice}}" stepKey="seeFirstInvoice"/> - <click selector="{{ViewOrderSection.transactions}}" stepKey="openTransactions"/> - <waitForPageLoad stepKey="waitForTransactions"/> - <see userInput="{{captureStatus}}" selector="{{ViewOrderSection.confirmCapture}}" stepKey="seeCapture"/> - <!-- Enable below line after fix of MC- - <see userInput="{{closedStatus}}" selector="{{ViewOrderSection.confirmClosed}}" stepKey="seeClosed"/> - --> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml deleted file mode 100644 index 59d4be98d450c..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?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="AuthorizenetAcceptjsOrderValidationData" type="AuthorizenetAcceptjsCredentials"> - <data key="virtualProductOrderAmount">$24.68</data> - <data key="twoSimpleProductsOrderAmount">$128.00</data> - <data key="processingStatusProcessing">Processing</data> - <data key="captureStatusCapture">Capture</data> - <data key="closedStatusNo">No</data> - </entity> -</entities> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml deleted file mode 100644 index defb91339ea8f..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?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="dashboard" type="button" selector="#menu-magento-backend-dashboard"/> - <element name="sales" type="button" selector="#menu-magento-sales-sales"/> - <element name="orders" type="button" selector="//*[@id='menu-magento-sales-sales']//span[text()='Orders']"/> - <element name="catalog" type="button" selector="#menu-magento-catalog-catalog"/> - <element name="customers" type="button" selector="#menu-magento-customer-customer"/> - <element name="marketing" type="button" selector="#menu-magento-backend-marketing"/> - <element name="content" type="button" selector="#menu-magento-backend-content"/> - <element name="reports" type="button" selector="#menu-magento-reports-report"/> - <element name="stores" type="button" selector="#menu-magento-backend-stores"/> - <element name="system" type="button" selector="#menu-magento-backend-system"/> - <element name="findPartners" type="button" selector="#menu-magento-marketplace-partners"/> - <element name="currencySetup" type="button" selector="//a[contains(@class, 'item-nav')]//span[text()='Currency Setup']"/> - </section> -</sections> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml deleted file mode 100644 index 31be865ea2678..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?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="AuthorizenetAcceptjsConfigurationSection"> - <element name="openSectionToggle" type="button" selector="#payment_us_authorizenet_acceptjs-head"/> - <element name="alreadyOpenSectionToggle" type="button" selector="#payment_us_authorizenet_acceptjs-head.open"/> - <element name="apiLoginIdField" type="input" selector="#payment_us_authorizenet_acceptjs_required_login"/> - <element name="transactionKeyField" type="input" selector="#payment_us_authorizenet_acceptjs_required_trans_key"/> - <element name="publicClientKeyField" type="input" selector="#payment_us_authorizenet_acceptjs_required_public_client_key"/> - <element name="signatureKeyField" type="input" selector="#payment_us_authorizenet_acceptjs_required_trans_signature_key"/> - <element name="enabledDefaultCheckbox" type="input" selector="#payment_us_authorizenet_acceptjs_active_inherit"/> - <element name="enabledDefaultSelect" type="select" selector="#payment_us_authorizenet_acceptjs_active"/> - <element name="paymentActionCheckbox" type="input" selector="#payment_us_authorizenet_acceptjs_required_payment_action_inherit"/> - <element name="paymentActionSelect" type="select" selector="#payment_us_authorizenet_acceptjs_required_payment_action"/> - <element name="paymentActionSelectDisabled" type="select" selector="#payment_us_authorizenet_acceptjs_required_payment_action[disabled='disabled']"/> - </section> -</sections> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml deleted file mode 100644 index 5d97842de374c..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?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="AuthorizenetCheckoutSection"> - <element name="selectAuthorizenet" type="button" selector="#authorizenet_acceptjs"/> - <element name="cardInput" type="input" selector="#authorizenet_acceptjs_cc_number"/> - <element name="expMonth" type="select" selector="#authorizenet_acceptjs_expiration"/> - <element name="expYear" type="select" selector="#authorizenet_acceptjs_expiration_yr"/> - <element name="cvv" type="input" selector="#authorizenet_acceptjs_cc_cid"/> - <element name="checkoutButton" type="button" selector="._active button.action.checkout"/> - </section> -</sections> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml deleted file mode 100644 index b5f2ecf641162..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?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="GuestAuthorizenetCheckoutSection"> - <element name="firstName" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.firstname'] input"/> - <element name="lastName" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.lastname'] input"/> - <element name="street" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.street.0'] input"/> - <element name="city" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.city'] input"/> - <element name="state" type="select" selector="div[name='billingAddressauthorizenet_acceptjs.region_id'] select"/> - <element name="postcode" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.postcode'] input"/> - <element name="country" type="select" selector="div[name='billingAddressauthorizenet_acceptjs.country_id'] select"/> - <element name="telephone" type="input" selector="div[name='billingAddressauthorizenet_acceptjs.telephone'] input"/> - <element name="update" type="button" selector=".payment-method._active button.action-update" /> - </section> -</sections> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml deleted file mode 100644 index 608067d7d31a1..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?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="ViewOrderSection"> - <element name="openInvoiceForm" type="button" selector="#order_invoice"/> - <element name="selectCaptureType" type="select" selector="select[name='invoice[capture_case]']"/> - <element name="submitInvoice" type="button" selector="button.submit-button"/> - <element name="commentsHistory" type="button" selector="#sales_order_view_tabs_order_history"/> - <element name="capturedAmountText" type="text" selector="//div[@class='comments-block-item'][2]/div[@class='comments-block-item-comment']"/> - <element name="capturedAmountTextUnsubmitted" type="text" selector="//div[@class='comments-block-item'][1]/div[@class='comments-block-item-comment']"/> - <element name="orderStatus" type="text" selector=".note-list-item .note-list-status"/> - <element name="invoices" type="button" selector="#sales_order_view_tabs_order_invoices"/> - <element name="firstInvoice" type="text" selector=".data-grid tbody tr"/> - <element name="transactions" type="button" selector="#sales_order_view_tabs_order_transactions"/> - <element name="confirmCapture" type="text" selector="//table[@id='order_transactions_table']/tbody/tr/td[6]"/> - <element name="confirmClosed" type="text" selector="//table[@id='order_transactions_table']/tbody/tr/td[7]"/> - </section> -</sections> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml deleted file mode 100644 index cbb702c26f17d..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?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="ConfigureAuthorizenetAcceptjsWithoutRequiredOptionsTest"> - <annotations> - <stories value="Authorize.net Accept.js"/> - <title value="Unable to configure Authorize.net Accept.js without required options"/> - <description value="Unable to configure Authorize.net Accept.js without required options"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-17805"/> - <useCaseId value="MC-17753"/> - <group value="AuthorizenetAcceptjs"/> - </annotations> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - </before> - <after> - <actionGroup ref="logout" stepKey="logout"/> - </after> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <actionGroup ref="EnableAuthorizenetAcceptjs" stepKey="enableAuthorizenetAcceptjs"/> - <actionGroup ref="AssertAuthorizenetAcceptjsRequiredFieldsValidationIsPresentOnSave" stepKey="assertErrorMessages"/> - </test> -</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml deleted file mode 100644 index 7f25482d627e1..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml +++ /dev/null @@ -1,77 +0,0 @@ -<?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="FullCaptureAuthorizenetAcceptjsTest"> - <annotations> - <stories value="Authorize.net Accept.js"/> - <title value="Full Capture using Authorize.net Accept.js"/> - <description value="Capture an order placed using Authorize.net Accept.js"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-12255"/> - <skip> - <issueId value="DEVOPS-4604"/> - </skip> - <group value="AuthorizenetAcceptjs"/> - <group value="ThirdPartyPayments"/> - </annotations> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - - <createData stepKey="createCustomer" entity="Simple_US_Customer"/> - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="_defaultProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> - </createData> - - <!--Configure Auth.net--> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> - <argument name="paymentAction" value="Authorize Only"/> - </actionGroup> - <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> - - </before> - <after> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <actionGroup ref="DisableAuthorizenetAcceptjs" stepKey="DisableAuthorizenetAcceptjs"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> - - <!--Storefront Login--> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginStorefront"> - <argument name="Customer" value="$$createCustomer$$"/> - </actionGroup> - - <!--Add product to cart--> - <amOnPage url="$$createProduct.name$$.html" stepKey="goToProductPage"/> - <waitForPageLoad stepKey="waitForProductPage"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForCartToFill"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> - - <!--Checkout steps--> - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> - <waitForPageLoad stepKey="waitForCheckoutLoad"/> - <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="submitShippingSelection"/> - <waitForPageLoad stepKey="waitForShippingToFinish"/> - <actionGroup ref="FillPaymentInformation" stepKey="fillPaymentInfo"/> - - <!--View and validate order--> - <actionGroup ref="ViewAndValidateOrderActionGroup" stepKey="viewAndValidateOrder"> - <argument name="amount" value="{{AuthorizenetAcceptjsOrderValidationData.twoSimpleProductsOrderAmount}}"/> - <argument name="status" value="{{AuthorizenetAcceptjsOrderValidationData.processingStatusProcessing}}"/> - <argument name="captureStatus" value="{{AuthorizenetAcceptjsOrderValidationData.captureStatusCapture}}"/> - <argument name="closedStatus" value="{{AuthorizenetAcceptjsOrderValidationData.closedStatusNo}}"/> - </actionGroup> - </test> -</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml deleted file mode 100644 index 919c32d8f70d6..0000000000000 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml +++ /dev/null @@ -1,86 +0,0 @@ -<?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="GuestCheckoutVirtualProductAuthorizenetAcceptjsTest"> - <annotations> - <stories value="Authorize.net Accept.js"/> - <title value="Guest Checkout of Virtual Product using Authorize.net Accept.js"/> - <description value="Checkout a virtual product with a guest using Authorize.net Accept.js"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-12712"/> - <skip> - <issueId value="DEVOPS-4604"/> - </skip> - <group value="AuthorizenetAcceptjs"/> - <group value="ThirdPartyPayments"/> - </annotations> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - - <!-- Create virtual product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> - <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> - <argument name="product" value="defaultVirtualProduct"/> - </actionGroup> - <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> - <argument name="product" value="defaultVirtualProduct"/> - </actionGroup> - <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> - - <!--Configure Auth.net--> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> - <argument name="paymentAction" value="Authorize and Capture"/> - </actionGroup> - <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> - - </before> - <after> - <actionGroup ref="DisableAuthorizenetAcceptjs" stepKey="DisableAuthorizenetAcceptjs"/> - <!-- Delete virtual product --> - <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> - <argument name="product" value="defaultVirtualProduct"/> - </actionGroup> - <actionGroup ref="logout" stepKey="logout"/> - </after> - - <!--Add product to cart twice--> - <amOnPage url="{{defaultVirtualProduct.sku}}.html" stepKey="goToProductPage"/> - <waitForPageLoad stepKey="waitForProductPage"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForCartToFill"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCartAgain"/> - <waitForPageLoad stepKey="waitForCartToFillAgain"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> - - <!--Checkout steps--> - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> - <waitForPageLoad stepKey="waitForCheckoutLoad"/> - - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="enterEmail"/> - <click stepKey="clickOnAuthorizenetToggle" selector="{{AuthorizenetCheckoutSection.selectAuthorizenet}}"/> - <waitForPageLoad stepKey="waitForBillingInfoLoad"/> - <actionGroup ref="GuestCheckoutAuthorizenetFillBillingAddress" stepKey="fillAddressForm"> - <argument name="customer" value="Simple_US_Customer"/> - <argument name="customerAddress" value="CustomerAddressSimple"/> - </actionGroup> - <actionGroup ref="FillPaymentInformation" stepKey="fillPaymentInfo"/> - - <!--View and validate order--> - <actionGroup ref="ViewAndValidateOrderActionGroupNoSubmit" stepKey="viewAndValidateOrder"> - <argument name="amount" value="{{AuthorizenetAcceptjsOrderValidationData.virtualProductOrderAmount}}"/> - <argument name="status" value="{{AuthorizenetAcceptjsOrderValidationData.processingStatusProcessing}}"/> - <argument name="captureStatus" value="{{AuthorizenetAcceptjsOrderValidationData.captureStatusCapture}}"/> - <argument name="closedStatus" value="{{AuthorizenetAcceptjsOrderValidationData.closedStatusNo}}"/> - </actionGroup> - </test> -</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php index da2b953d843b1..646ad4f195b9d 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php @@ -81,6 +81,9 @@ public function testGetSolutionIdSandbox($environment, $expectedSolution) $this->assertEquals($expectedSolution, $this->model->getSolutionId(123)); } + /** + * @return array + */ public function configMapProvider() { return [ @@ -97,6 +100,10 @@ public function configMapProvider() ['getTransactionInfoSyncKeys', 'transactionSyncKeys', 'a,b,c', ['a', 'b', 'c']], ]; } + + /** + * @return array + */ public function environmentUrlProvider() { return [ @@ -105,6 +112,9 @@ public function environmentUrlProvider() ]; } + /** + * @return array + */ public function environmentSolutionProvider() { return [ diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php index 6ddb30a64af96..84c2f19040e16 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php @@ -108,6 +108,10 @@ public function testBuildWithBothAddresses() $this->assertEquals('abc', $result['transactionRequest']['customerIP']); } + /** + * @param $responseData + * @param $addressPrefix + */ private function validateAddressData($responseData, $addressPrefix) { foreach ($this->mockAddressData as $fieldValue => $field) { @@ -115,6 +119,11 @@ private function validateAddressData($responseData, $addressPrefix) } } + /** + * @param $prefix + * + * @return \PHPUnit\Framework\MockObject\MockObject + */ private function createAddressMock($prefix) { $addressAdapterMock = $this->createMock(AddressAdapterInterface::class); diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php index a52a1b317fbb7..197dc209ece66 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php @@ -112,6 +112,9 @@ public function testDoesNothingWhenPending(string $status) $this->handler->handle($subject, $response); } + /** + * @return array + */ public function pendingTransactionStatusesProvider() { return [ @@ -120,6 +123,9 @@ public function pendingTransactionStatusesProvider() ]; } + /** + * @return array + */ public function declinedTransactionStatusesProvider() { return [ diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml index 8623919cf5d6b..7cd00959d9772 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml @@ -9,7 +9,7 @@ <system> <section id="payment"> <group id="authorizenet_acceptjs" translate="label" type="text" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Authorize.Net</label> + <label>Authorize.Net (Deprecated)</label> <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> diff --git a/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv index 3c5b677c88cc8..a8b5dbd2df525 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv +++ b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv @@ -1,11 +1,11 @@ -Authorize.net,Authorize.net +"Authorize.Net (Deprecated)","Authorize.Net (Deprecated)" "Gateway URL","Gateway URL" "Invalid payload type.","Invalid payload type." "Something went wrong in the payment gateway.","Something went wrong in the payment gateway." "Merchant MD5 (deprecated","Merchant MD5 (deprecated" "Signature Key","Signature Key" "Basic Authorize.Net Settings","Basic Authorize.Net Settings" -"Advanced Authorie.Net Settings","Advanced Authorie.Net Settings" +"Advanced Authorize.Net Settings","Advanced Authorize.Net Settings" "Public Client Key","Public Client Key" "Environment","Environment" "Production","Production" diff --git a/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php index 00def8ce2b0cf..bf8e1661a3f61 100644 --- a/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php +++ b/app/code/Magento/AuthorizenetCardinal/Gateway/Request/Authorize3DSecureBuilder.php @@ -16,6 +16,9 @@ /** * Adds the cardholder authentication information to the request + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Authorize3DSecureBuilder implements BuilderInterface { diff --git a/app/code/Magento/AuthorizenetCardinal/Gateway/Validator/CavvResponseValidator.php b/app/code/Magento/AuthorizenetCardinal/Gateway/Validator/CavvResponseValidator.php index 036c1fa332ebf..35287406a12d5 100644 --- a/app/code/Magento/AuthorizenetCardinal/Gateway/Validator/CavvResponseValidator.php +++ b/app/code/Magento/AuthorizenetCardinal/Gateway/Validator/CavvResponseValidator.php @@ -16,6 +16,9 @@ /** * Validates cardholder authentication verification response code. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class CavvResponseValidator extends AbstractValidator { diff --git a/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php b/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php index d0cde9c643ebf..8f09395874dce 100644 --- a/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php +++ b/app/code/Magento/AuthorizenetCardinal/Model/Checkout/ConfigProvider.php @@ -12,6 +12,9 @@ /** * Configuration provider. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class ConfigProvider implements ConfigProviderInterface { diff --git a/app/code/Magento/AuthorizenetCardinal/Model/Config.php b/app/code/Magento/AuthorizenetCardinal/Model/Config.php index e70a6a2e39c1f..798fb846c160e 100644 --- a/app/code/Magento/AuthorizenetCardinal/Model/Config.php +++ b/app/code/Magento/AuthorizenetCardinal/Model/Config.php @@ -14,6 +14,9 @@ * AuthorizenetCardinal integration configuration. * * Class is a proxy service for retrieving configuration settings. + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class Config { diff --git a/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php b/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php index cb2cdf64ae389..aa5fbee327fe5 100644 --- a/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php +++ b/app/code/Magento/AuthorizenetCardinal/Observer/DataAssignObserver.php @@ -15,11 +15,14 @@ /** * Adds the payment info to the payment object + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class DataAssignObserver extends AbstractDataAssignObserver { /** - * JWT key + * Cardinal JWT key */ private const JWT_KEY = 'cardinalJWT'; diff --git a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php index 207d21994308f..ffbacbf6ac88c 100644 --- a/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php +++ b/app/code/Magento/AuthorizenetGraphQl/Model/AuthorizenetDataProvider.php @@ -9,9 +9,13 @@ use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** * SetPaymentMethod additional data provider model for Authorizenet payment method + * + * @deprecated Starting from Magento 2.3.4 Authorize.net payment method core integration is deprecated in favor of + * official payment integration available on the marketplace */ class AuthorizenetDataProvider implements AdditionalDataProviderInterface { @@ -36,10 +40,32 @@ public function __construct( * * @param array $data * @return array + * @throws GraphQlInputException */ public function getData(array $data): array { - $additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data) ?? []; + if (!isset($data[self::PATH_ADDITIONAL_DATA])) { + throw new GraphQlInputException( + __('Required parameter "authorizenet_acceptjs" for "payment_method" is missing.') + ); + } + if (!isset($data[self::PATH_ADDITIONAL_DATA]['opaque_data_descriptor'])) { + throw new GraphQlInputException( + __('Required parameter "opaque_data_descriptor" for "authorizenet_acceptjs" is missing.') + ); + } + if (!isset($data[self::PATH_ADDITIONAL_DATA]['opaque_data_value'])) { + throw new GraphQlInputException( + __('Required parameter "opaque_data_value" for "authorizenet_acceptjs" is missing.') + ); + } + if (!isset($data[self::PATH_ADDITIONAL_DATA]['cc_last_4'])) { + throw new GraphQlInputException( + __('Required parameter "cc_last_4" for "authorizenet_acceptjs" is missing.') + ); + } + + $additionalData = $this->arrayManager->get(static::PATH_ADDITIONAL_DATA, $data); foreach ($additionalData as $key => $value) { $additionalData[$this->convertSnakeCaseToCamelCase($key)] = $value; unset($additionalData[$key]); diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php index bca7f13b0cee3..0a73430aad0f3 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php +++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php @@ -19,21 +19,21 @@ class Grid extends \Magento\Backend\Block\Dashboard\Grid protected $_collectionFactory; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory, array $data = [] ) { diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php index 3455ff087a799..b388339460102 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Sales.php +++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php @@ -18,20 +18,20 @@ class Sales extends \Magento\Backend\Block\Dashboard\Bar protected $_template = 'Magento_Backend::dashboard/salebar.phtml'; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_moduleManager = $moduleManager; diff --git a/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php b/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php index 7dc897a62a320..a0b1571bd17bb 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php +++ b/app/code/Magento/Backend/Block/Dashboard/Tab/Products/Ordered.php @@ -19,21 +19,21 @@ class Ordered extends \Magento\Backend\Block\Dashboard\Grid protected $_collectionFactory; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Sales\Model\ResourceModel\Report\Bestsellers\CollectionFactory $collectionFactory * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Sales\Model\ResourceModel\Report\Bestsellers\CollectionFactory $collectionFactory, array $data = [] ) { diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php index e57a6249af47d..20bcfebe31a8d 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Totals.php +++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php @@ -22,20 +22,20 @@ class Totals extends \Magento\Backend\Block\Dashboard\Bar protected $_template = 'Magento_Backend::dashboard/totalbar.phtml'; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Reports\Model\ResourceModel\Order\CollectionFactory $collectionFactory, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_moduleManager = $moduleManager; diff --git a/app/code/Magento/Backend/Block/Store/Switcher.php b/app/code/Magento/Backend/Block/Store/Switcher.php index 9c35cfb5df81d..2b9f70844df86 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher.php +++ b/app/code/Magento/Backend/Block/Store/Switcher.php @@ -592,7 +592,7 @@ public function getHintHtml() 'What is this?' ) . '"' . ' class="admin__field-tooltip-action action-help"><span>' . __( 'What is this?' - ) . '</span></a></span>' . ' </div>'; + ) . '</span></a>' . ' </div>'; } return $html; } diff --git a/app/code/Magento/Backend/Block/System/Account/Edit/Form.php b/app/code/Magento/Backend/Block/System/Account/Edit/Form.php index 7c5246143b2c6..c075585a6e4eb 100644 --- a/app/code/Magento/Backend/Block/System/Account/Edit/Form.php +++ b/app/code/Magento/Backend/Block/System/Account/Edit/Form.php @@ -68,7 +68,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareForm() { @@ -114,7 +114,7 @@ protected function _prepareForm() 'name' => 'password', 'label' => __('New Password'), 'title' => __('New Password'), - 'class' => 'validate-admin-password admin__control-text' + 'class' => 'validate-admin-password' ] ); @@ -124,7 +124,7 @@ protected function _prepareForm() [ 'name' => 'password_confirmation', 'label' => __('Password Confirmation'), - 'class' => 'validate-cpassword admin__control-text' + 'class' => 'validate-cpassword' ] ); @@ -152,7 +152,7 @@ protected function _prepareForm() 'label' => __('Your Password'), 'id' => self::IDENTITY_VERIFICATION_PASSWORD_FIELD, 'title' => __('Your Password'), - 'class' => 'validate-current-password required-entry admin__control-text', + 'class' => 'validate-current-password required-entry', 'required' => true ] ); diff --git a/app/code/Magento/Backend/Block/Widget/Grid.php b/app/code/Magento/Backend/Block/Widget/Grid.php index 66298d23389fb..86ad00bfaa7ca 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid.php +++ b/app/code/Magento/Backend/Block/Widget/Grid.php @@ -300,7 +300,10 @@ protected function _addColumnFilterToCollection($column) if ($this->getCollection()) { $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex(); if ($column->getFilterConditionCallback()) { - call_user_func($column->getFilterConditionCallback(), $this->getCollection(), $column); + $column->getFilterConditionCallback()[0]->{$column->getFilterConditionCallback()[1]}( + $this->getCollection(), + $column + ); } else { $condition = $column->getFilter()->getCondition(); if ($field && isset($condition)) { @@ -363,7 +366,7 @@ protected function _prepareCollection() $this->_setFilterValues($data); } elseif ($filter && is_array($filter)) { $this->_setFilterValues($filter); - } elseif (0 !== sizeof($this->_defaultFilter)) { + } elseif (0 !== count($this->_defaultFilter)) { $this->_setFilterValues($this->_defaultFilter); } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php index f4558594332c3..a7d85a4cfef4c 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php @@ -47,7 +47,7 @@ public function render(\Magento\Framework\DataObject $row) return ' '; } - if (sizeof($actions) == 1 && !$this->getColumn()->getNoLink()) { + if (count($actions) == 1 && !$this->getColumn()->getNoLink()) { foreach ($actions as $action) { if (is_array($action)) { return $this->_toLinkHtml($action, $row); @@ -104,6 +104,7 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) $this->_transformActionData($action, $actionCaption, $row); if (isset($action['confirm'])) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $action['onclick'] = 'return window.confirm(\'' . addslashes( $this->escapeHtml($action['confirm']) ) . '\')'; @@ -117,8 +118,8 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) /** * Prepares action data for html render * - * @param array &$action - * @param string &$actionCaption + * @param &array $action + * @param &string $actionCaption * @param \Magento\Framework\DataObject $row * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -144,7 +145,7 @@ protected function _transformActionData(&$action, &$actionCaption, \Magento\Fram if (is_array($action['url']) && isset($action['field'])) { $params = [$action['field'] => $this->_getValue($row)]; if (isset($action['url']['params'])) { - $params = array_merge($action['url']['params'], $params); + $params[] = $action['url']['params']; } $action['href'] = $this->getUrl($action['url']['base'], $params); unset($action['field']); 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 891b2a3ada724..284cb01148f68 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -3,26 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Backend\Block\Widget\Grid\Massaction; +use Magento\Backend\Block\Template\Context; +use Magento\Backend\Block\Widget; +use Magento\Backend\Block\Widget\Grid\Column; +use Magento\Backend\Block\Widget\Grid\ColumnSet; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Framework\Json\EncoderInterface; +use Magento\Quote\Model\Quote; /** * Grid widget massaction block * + * phpcs:disable Magento2.Classes.AbstractApi * @api - * @method \Magento\Quote\Model\Quote setHideFormElement(boolean $value) Hide Form element to prevent IE errors + * @method Quote setHideFormElement(boolean $value) Hide Form element to prevent IE errors * @method boolean getHideFormElement() * @deprecated 100.2.0 in favour of UI component implementation * @since 100.0.2 */ -abstract class AbstractMassaction extends \Magento\Backend\Block\Widget +abstract class AbstractMassaction extends Widget { /** - * @var \Magento\Framework\Json\EncoderInterface + * @var EncoderInterface */ protected $_jsonEncoder; @@ -39,13 +48,13 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget protected $_template = 'Magento_Backend::widget/grid/massaction.phtml'; /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param Context $context + * @param EncoderInterface $jsonEncoder * @param array $data */ public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Framework\Json\EncoderInterface $jsonEncoder, + Context $context, + EncoderInterface $jsonEncoder, array $data = [] ) { $this->_jsonEncoder = $jsonEncoder; @@ -122,11 +131,7 @@ private function isVisible(DataObject $item) */ public function getItem($itemId) { - if (isset($this->_items[$itemId])) { - return $this->_items[$itemId]; - } - - return null; + return $this->_items[$itemId] ?? null; } /** @@ -161,7 +166,7 @@ public function getItemsJson() */ public function getCount() { - return sizeof($this->_items); + return count($this->_items); } /** @@ -288,11 +293,11 @@ public function getGridIdsJson() if ($collection instanceof AbstractDb) { $idsSelect = clone $collection->getSelect(); - $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); - $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); - $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); - $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); - $idsSelect->columns($this->getMassactionIdField(), 'main_table'); + $idsSelect->reset(Select::ORDER); + $idsSelect->reset(Select::LIMIT_COUNT); + $idsSelect->reset(Select::LIMIT_OFFSET); + $idsSelect->reset(Select::COLUMNS); + $idsSelect->columns($this->getMassactionIdField()); $idList = $collection->getConnection()->fetchCol($idsSelect); } else { $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); @@ -358,7 +363,7 @@ public function prepareMassactionColumn() { $columnId = 'massaction'; $massactionColumn = $this->getLayout()->createBlock( - \Magento\Backend\Block\Widget\Grid\Column::class + Column::class )->setData( [ 'index' => $this->getMassactionIdField(), @@ -378,7 +383,7 @@ public function prepareMassactionColumn() $gridBlock = $this->getParentBlock(); $massactionColumn->setSelected($this->getSelected())->setGrid($gridBlock)->setId($columnId); - /** @var $columnSetBlock \Magento\Backend\Block\Widget\Grid\ColumnSet */ + /** @var $columnSetBlock ColumnSet */ $columnSetBlock = $gridBlock->getColumnSet(); $childNames = $columnSetBlock->getChildNames(); $siblingElement = count($childNames) ? current($childNames) : 0; diff --git a/app/code/Magento/Backend/Helper/Dashboard/AbstractDashboard.php b/app/code/Magento/Backend/Helper/Dashboard/AbstractDashboard.php index 6eb3ecb3049a9..7cb8690b4ec27 100644 --- a/app/code/Magento/Backend/Helper/Dashboard/AbstractDashboard.php +++ b/app/code/Magento/Backend/Helper/Dashboard/AbstractDashboard.php @@ -8,6 +8,7 @@ /** * Adminhtml abstract dashboard helper. * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -28,6 +29,8 @@ abstract class AbstractDashboard extends \Magento\Framework\App\Helper\AbstractH protected $_params = []; /** + * Return collections + * * @return array|\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getCollection() @@ -39,6 +42,8 @@ public function getCollection() } /** + * Init collections + * * @return void */ abstract protected function _initCollection(); @@ -54,14 +59,18 @@ public function getItems() } /** + * Return items count + * * @return int */ public function getCount() { - return sizeof($this->getItems()); + return count($this->getItems()); } /** + * Return column + * * @param string $index * @return array */ @@ -85,6 +94,8 @@ public function getColumn($index) } /** + * Set params with value + * * @param string $name * @param mixed $value * @return void @@ -95,6 +106,8 @@ public function setParam($name, $value) } /** + * Set params + * * @param array $params * @return void */ @@ -104,6 +117,8 @@ public function setParams(array $params) } /** + * Get params with name + * * @param string $name * @return mixed */ @@ -117,6 +132,8 @@ public function getParam($name) } /** + * Get params + * * @return array */ public function getParams() diff --git a/app/code/Magento/Backend/Helper/Dashboard/Data.php b/app/code/Magento/Backend/Helper/Dashboard/Data.php index 29bffbd6a9dc2..c06e7ea3ba38f 100644 --- a/app/code/Magento/Backend/Helper/Dashboard/Data.php +++ b/app/code/Magento/Backend/Helper/Dashboard/Data.php @@ -68,7 +68,7 @@ public function getStores() */ public function countStores() { - return sizeof($this->_stores->getItems()); + return count($this->_stores->getItems()); } /** @@ -88,8 +88,7 @@ public function getDatePeriods() } /** - * Create data hash to ensure that we got valid - * data and it is not changed by some one else. + * Create data hash to ensure that we got valid data and it is not changed by some one else. * * @param string $data * @return string @@ -97,6 +96,7 @@ public function getDatePeriods() public function getChartDataHash($data) { $secret = $this->_installDate; + // phpcs:disable Magento2.Security.InsecureFunction.FoundWithAlternative return md5($data . $secret); } } diff --git a/app/code/Magento/Backend/Model/Locale/Resolver.php b/app/code/Magento/Backend/Model/Locale/Resolver.php index b9be471cd5990..9086e2af83e24 100644 --- a/app/code/Magento/Backend/Model/Locale/Resolver.php +++ b/app/code/Magento/Backend/Model/Locale/Resolver.php @@ -7,8 +7,10 @@ /** * Backend locale model + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Resolver extends \Magento\Framework\Locale\Resolver { @@ -40,7 +42,7 @@ class Resolver extends \Magento\Framework\Locale\Resolver * @param Manager $localeManager * @param \Magento\Framework\App\RequestInterface $request * @param \Magento\Framework\Validator\Locale $localeValidator - * @param null $locale + * @param string|null $locale * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -76,7 +78,7 @@ public function setLocale($locale = null) $sessionLocale = $this->_session->getSessionLocale(); $userLocale = $this->_localeManager->getUserInterfaceLocale(); - $localeCodes = array_filter([$forceLocale, $sessionLocale, $userLocale]); + $localeCodes = array_filter([$forceLocale, $locale, $sessionLocale, $userLocale]); if (count($localeCodes)) { $locale = reset($localeCodes); diff --git a/app/code/Magento/Backend/Model/Menu/Item.php b/app/code/Magento/Backend/Model/Menu/Item.php index d535e9c84df24..67c6216cbbc06 100644 --- a/app/code/Magento/Backend/Model/Menu/Item.php +++ b/app/code/Magento/Backend/Model/Menu/Item.php @@ -145,7 +145,7 @@ class Item protected $_moduleList; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $_moduleManager; @@ -163,7 +163,7 @@ class Item * @param \Magento\Backend\Model\MenuFactory $menuFactory * @param \Magento\Backend\Model\UrlInterface $urlModel * @param \Magento\Framework\Module\ModuleListInterface $moduleList - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( @@ -173,7 +173,7 @@ public function __construct( \Magento\Backend\Model\MenuFactory $menuFactory, \Magento\Backend\Model\UrlInterface $urlModel, \Magento\Framework\Module\ModuleListInterface $moduleList, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_validator = $validator; diff --git a/app/code/Magento/Backend/Model/Menu/Item/Validator.php b/app/code/Magento/Backend/Model/Menu/Item/Validator.php index 7b72b355f551d..62225c5707c0d 100644 --- a/app/code/Magento/Backend/Model/Menu/Item/Validator.php +++ b/app/code/Magento/Backend/Model/Menu/Item/Validator.php @@ -6,6 +6,9 @@ namespace Magento\Backend\Model\Menu\Item; /** + * Class Validator + * + * @package Magento\Backend\Model\Menu\Item * @api * @since 100.0.2 */ @@ -49,7 +52,7 @@ public function __construct() $attributeValidator = new \Zend_Validate(); $attributeValidator->addValidator(new \Zend_Validate_StringLength(['min' => 3])); - $attributeValidator->addValidator(new \Zend_Validate_Regex('/^[A-Za-z0-9\/_]+$/')); + $attributeValidator->addValidator(new \Zend_Validate_Regex('/^[A-Za-z0-9\/_\-]+$/')); $textValidator = new \Zend_Validate_StringLength(['min' => 3, 'max' => 50]); @@ -101,6 +104,7 @@ private function checkMenuItemIsRemoved($data) /** * Check that menu item contains all required data + * * @param array $data * * @throws \BadMethodCallException diff --git a/app/code/Magento/Backend/README.md b/app/code/Magento/Backend/README.md index 03c7d86516b92..205051809328a 100644 --- a/app/code/Magento/Backend/README.md +++ b/app/code/Magento/Backend/README.md @@ -1,3 +1,112 @@ -The Backend module contains common infrastructure and assets for other modules to be defined and used in their -administration user interface (UI). It does not contain anything specific to other modules. Among many things it -handles the logic of authenticating and authorizing users. +# Magento_Backend module + +The Magento_Backend module contains common infrastructure and assets for other modules to be defined and used in their +administration user interface (UI). + +The Magento_Backend module does not contain anything specific to other modules. Among many things it handles the logic of authenticating and authorizing users. + +## Installation details + +Before disabling or uninstalling this module, note that the following modules depends on this module: + +- Magento_Analytics +- Magento_Authorization +- Magento_NewRelicReporting +- Magento_ProductVideo +- Magento_ReleaseNotification +- Magento_Search +- Magento_Security +- Magento_Signifyd +- Magento_Swatches +- Magento_Ui +- Magento_User +- Magento_Webapi + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Structure + +Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.3/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `Service/V1`. + +`Service/V1` - contains logic to provide a list of modules installed in Magento. + +For information about typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/build/module-file-structure.html#module-file-structure). + +## Extensibility + +Extension developers can interact with the Magento_Backend module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backend module. + +### Events + +The module dispatches the following events: + + - `adminhtml_block_html_before` event in the `\Magento\Backend\Block\Template::_toHtml()` method. Parameters: + - `block` is the backend block template (this) (`\Magento\Backend\Block\Template` class). + - `adminhtml_store_edit_form_prepare_form` event in the `\Magento\Backend\Block\System\Store\Edit\AbstractForm::_prepareForm()` method. Parameters: + - `block` is the AbstractForm block (this) (`\Magento\Backend\Block\System\Store\Edit\AbstractForm` class). + - `backend_block_widget_grid_prepare_grid_before` event in the `\Magento\Backend\Block\Widget\Grid::_prepareGrid()` method. Parameters: + - `grid` is the widget grid block (this) (`\Magento\Backend\Block\Widget\Grid` class) + - `collection` is the grid collection (`\Magento\Framework\Data\Collection` class). + - `adminhtml_cache_flush_system` event in the `\Magento\Backend\Console\Command\CacheCleanCommand::performAction()` method. + - `adminhtml_cache_flush_all` event in the `\Magento\Backend\Console\Command\CacheFlushCommand::performAction()` method. + - `clean_catalog_images_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanImages::execute()` method. + - `clean_media_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanMedia::execute()` method. + - `clean_static_files_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanStaticFiles::execute()` method. + - `adminhtml_cache_flush_all` event in the `\Magento\Backend\Controller\Adminhtml\Cache\FlushAll::execute()` method. + - `adminhtml_cache_flush_system` event in the `\Magento\Backend\Controller\Adminhtml\Cache\FlushSystem::execute()` method. + - `theme_save_after` event in the `\Magento\Backend\Controller\Adminhtml\System\Design\Save::execute()` method. + - `backend_auth_user_login_success` event in the `\Magento\Backend\Model\Auth::login()` method. Parameters: + - `user` is the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`) + - `backend_auth_user_login_failed` event in the `\Magento\Backend\Model\Auth::login()` method. Parameters: + - `user_name` is username extracted from the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`) + - `exception` any exception generated (`\Magento\Framework\Exception\LocalizedException | \Magento\Framework\Exception\Plugin\AuthenticationException`) + +For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `admin_login` +- `adminhtml_auth_login` +- `adminhtml_cache_block` +- `adminhtml_cache_index` +- `adminhtml_dashboard_customersmost` +- `adminhtml_dashboard_customersnewest` +- `adminhtml_dashboard_index` +- `adminhtml_dashboard_productsviewed` +- `adminhtml_denied` +- `adminhtml_noroute` +- `adminhtml_system_account_index` +- `adminhtml_system_design_edit` +- `adminhtml_system_design_grid` +- `adminhtml_system_design_grid_block` +- `adminhtml_system_design_index` +- `adminhtml_system_store_deletestore` +- `adminhtml_system_store_editstore` +- `adminhtml_system_store_grid_block` +- `adminhtml_system_store_index` +- `default` +- `editor` +- `empty` +- `formkey` +- `overlay_popup` +- `popup` + + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +### UI components + +You can extend Magento_Backend module using the following configuration files: + +- `view/adminhtml/ui_component/design_config_form.xml` +- `view/adminhtml/ui_component/design_config_listing.xml` + +For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml index 37fd1cb17ffe3..8d8a3f005d147 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml @@ -17,6 +17,7 @@ <argument name="submenuUiId" type="string"/> </arguments> + <waitForPageLoad stepKey="waitPageLoad"/> <click selector="{{AdminMenuSection.menuItem(menuUiId)}}" stepKey="clickOnMenuItem"/> <click selector="{{AdminMenuSection.menuItem(submenuUiId)}}" stepKey="clickOnSubmenuItem"/> </actionGroup> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminUserIsInGridActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminUserIsInGridActionGroup.xml new file mode 100644 index 0000000000000..3499f4e0d951c --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminUserIsInGridActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertAdminUserIsInGridActionGroup"> + <arguments> + <argument name="user" type="entity"/> + </arguments> + <click selector="{{AdminUserGridSection.resetButton}}" stepKey="resetGridFilter"/> + <waitForPageLoad stepKey="waitForFiltersReset" time="15"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="{{user.username}}" stepKey="enterUserName"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearch"/> + <waitForPageLoad stepKey="waitForGridToLoad" time="15"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="{{user.username}}" stepKey="seeUser"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertUserRoleRestrictedAccessActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertUserRoleRestrictedAccessActionGroup.xml new file mode 100644 index 0000000000000..0747eab31588e --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertUserRoleRestrictedAccessActionGroup.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="AssertUserRoleRestrictedAccessActionGroup"> + <see selector="{{AdminHeaderSection.pageHeading}}" userInput="Sorry, you need permissions to view this content." stepKey="seeErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml b/app/code/Magento/Backend/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml new file mode 100644 index 0000000000000..a8db2f94d69ab --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml @@ -0,0 +1,33 @@ +<?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="AdminGeneralSetStoreNameConfigData"> + <data key="path">general/store_information/name</data> + <data key="value">New Store Information</data> + </entity> + <entity name="AdminGeneralSetStorePhoneConfigData"> + <data key="path">general/store_information/phone</data> + </entity> + <entity name="AdminGeneralSetCountryConfigData"> + <data key="path">general/store_information/country_id</data> + </entity> + <entity name="AdminGeneralSetCityConfigData"> + <data key="path">general/store_information/city</data> + </entity> + <entity name="AdminGeneralSetPostcodeConfigData"> + <data key="path">general/store_information/postcode</data> + </entity> + <entity name="AdminGeneralSetStreetAddressConfigData"> + <data key="path">general/store_information/street_line1</data> + </entity> + <entity name="AdminGeneralSetStreetAddress2ConfigData"> + <data key="path">general/store_information/street_line2</data> + </entity> +</entities> diff --git a/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml b/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml index 52a6c27a37ea8..a844e962202f8 100644 --- a/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml +++ b/app/code/Magento/Backend/Test/Mftf/Data/CookieConfigData.xml @@ -20,4 +20,20 @@ <data key="scope_code">base</data> <data key="value">''</data> </entity> + <entity name="ChangeWebCookieLifetimeConfigData"> + <data key="path">web/cookie/cookie_lifetime</data> + <data key="value">60</data> + </entity> + <entity name="DefaultWebCookieLifetimeConfigData"> + <data key="path">web/cookie/cookie_lifetime</data> + <data key="value">3600</data> + </entity> + <entity name="ChangeAdminSecuritySessionLifetimeConfigData"> + <data key="path">admin/security/session_lifetime</data> + <data key="value">60</data> + </entity> + <entity name="DefaultAdminSecuritySessionLifetimeConfigData"> + <data key="path">admin/security/session_lifetime</data> + <data key="value">7200</data> + </entity> </entities> diff --git a/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.xml b/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.xml new file mode 100644 index 0000000000000..22d595c39407f --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.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="GeneralLocalCodeConfigsForChina"> + <data key="path">general/locale/code</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + <data key="value">zh_Hans_CN</data> + </entity> + <entity name="GeneralLocalCodeConfigsForUS"> + <data key="path">general/locale/code</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + <data key="value">en_US</data> + </entity> +</entities> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml index bd65dea89abc2..0ad63c0f0d23a 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml @@ -13,5 +13,6 @@ <element name="password" type="input" selector="#login"/> <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> <element name="forgotPasswordLink" type="button" selector=".action-forgotpassword" timeout="10"/> + <element name="loginBlock" type="block" selector=".adminhtml-auth-login"/> </section> </sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml index be3ef92acf0ac..bb1123d01c867 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml @@ -14,5 +14,13 @@ <element name="error" type="text" selector="#messages div.message-error"/> <element name="notice" type="text" selector=".message.message-notice.notice"/> <element name="messageByType" type="text" selector="#messages div.message-{{messageType}}" parameterized="true" /> + <element name="warning" type="text" selector="#messages div.message-warning"/> + <element name="accessDenied" type="text" selector=".access-denied-page"/> + <!-- Deprecated elements, please do not use them. Use elements above--> + <!-- Elements below are too common and catch non messages blocks. Ex: system messages blocks--> + <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message.message-error.error"/> + <element name="warningMessage" type="text" selector=".message-warning"/> + <element name="noticeMessage" type="text" selector=".message-notice"/> </section> </sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml index a460aaebf1051..5aaefc383f413 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -11,6 +11,10 @@ <section name="LocaleOptionsSection"> <element name="sectionHeader" type="text" selector="#general_locale-head"/> <element name="timezone" type="select" selector="#general_locale_timezone"/> + <element name="locale" type="select" selector="#general_locale_code"/> + <element name="localeEnabled" type="select" selector="#general_locale_code:enabled"/> + <element name="localeDisabled" type="select" selector="#general_locale_code[disabled=disabled]"/> <element name="useDefault" type="checkbox" selector="#general_locale_timezone_inherit"/> + <element name="defaultLocale" type="checkbox" selector="#general_locale_code_inherit"/> </section> </sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml new file mode 100644 index 0000000000000..47b8715b5541c --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml @@ -0,0 +1,38 @@ +<?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="AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Check locale dropdown and developer configuration page are available in developer mode"/> + <description value="Check locale dropdown and developer configuration page are available in developer mode"/> + <group value="backend"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20374"/> + <group value="developer_mode_only"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Go to the general configuration and make sure the locale dropdown is available and enabled --> + <actionGroup ref="AdminOpenStoreConfigPageActionGroup" stepKey="openStoreConfigPage" /> + <scrollTo selector="{{LocaleOptionsSection.sectionHeader}}" stepKey="scrollToLocaleSection" x="0" y="-80" /> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <seeElement selector="{{LocaleOptionsSection.localeEnabled}}" stepKey="seeEnabledLocaleDropdown"/> + + <!-- Go to the developer configuration and make sure the page is available --> + <actionGroup ref="AdminOpenStoreConfigDeveloperPageActionGroup" stepKey="goToDeveloperConfigPage"/> + <seeInCurrentUrl url="{{AdminConfigDeveloperPage.url}}" stepKey="seeDeveloperConfigUrl"/> + <seeElement selector="{{AdminConfigSection.navItemByTitle('Developer')}}" stepKey="assertDeveloperNavItemPresent" /> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml new file mode 100644 index 0000000000000..ae7722b225cdd --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml @@ -0,0 +1,42 @@ +<?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="AdminCheckLocaleAndDeveloperConfigInProductionModeTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Check locale dropdown and developer configuration page are not available in production mode"/> + <description value="Check locale dropdown and developer configuration page are not available in production mode"/> + <testCaseId value="MC-14106" /> + <severity value="MAJOR"/> + <group value="backend"/> + <group value="production_mode_only"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Go to the general configuration and make sure the locale dropdown is disabled --> + <actionGroup ref="AdminOpenStoreConfigPageActionGroup" stepKey="openStoreConfigPage" /> + <scrollTo selector="{{LocaleOptionsSection.sectionHeader}}" stepKey="scrollToLocaleSection" x="0" y="-80" /> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <assertElementContainsAttribute selector="{{LocaleOptionsSection.locale}}" attribute="disabled" stepKey="seeDisabledLocaleDropdown" /> + + <!-- Go to the developer configuration and make sure the redirect to the configuration page takes place --> + <actionGroup ref="AdminOpenStoreConfigDeveloperPageActionGroup" stepKey="goToDeveloperConfigPage"/> + <seeInCurrentUrl url="{{AdminConfigPage.url}}index/" stepKey="seeConfigurationIndexUrl"/> + + <actionGroup ref="AdminExpandConfigTabActionGroup" stepKey="expandAdvancedTab"> + <argument name="tabName" value="Advanced" /> + </actionGroup> + <dontSeeElement selector="{{AdminConfigSection.navItemByTitle('Developer')}}" stepKey="assertDeveloperNavItemAbsent" /> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml index 33561d7c3b03e..6434d74b28754 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml @@ -25,6 +25,7 @@ <after> <actionGroup ref="logout" stepKey="logout"/> </after> + <waitForPageLoad stepKey="waitForPageLoad"/> <click selector="{{AdminMenuSection.menuItem(AdminMenuDashboard.dataUiId)}}" stepKey="clickOnMenuItem"/> <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> <argument name="title" value="{{AdminMenuDashboard.pageTitle}}"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml new file mode 100644 index 0000000000000..88d26c052b59b --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml @@ -0,0 +1,37 @@ +<?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="AdminExpireAdminSessionTest"> + <annotations> + <features value="Backend"/> + <stories value="Admin Session Expire"/> + <title value="Admin Session Expire"/> + <description value="Admin Session Expire"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14111"/> + <group value="Backend"/> + <group value="mtf_migrated"/> + </annotations> + <after> + <!-- 4. Restore default configuration settings. --> + <magentoCLI command="config:set {{DefaultAdminSecuritySessionLifetimeConfigData.path}} {{DefaultAdminSecuritySessionLifetimeConfigData.value}}" stepKey="setDefaultSessionLifetime"/> + </after> + <!-- 1. Apply configuration settings. --> + <magentoCLI command="config:set {{ChangeAdminSecuritySessionLifetimeConfigData.path}} {{ChangeAdminSecuritySessionLifetimeConfigData.value}}" stepKey="changeCookieLifetime"/> + + <!-- 2. Wait for session to expire. --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <wait time="60" stepKey="waitForSessionLifetime"/> + <reloadPage stepKey="reloadPage"/> + + <!-- 3. Perform asserts. --> + <seeElement selector="{{AdminLoginFormSection.loginBlock}}" stepKey="assertAdminLoginPageIsAvailable"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml new file mode 100644 index 0000000000000..88646401e3a99 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml @@ -0,0 +1,48 @@ +<?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="AdminExpireCustomerSessionTest"> + <annotations> + <features value="Backend"/> + <stories value="Customer Session Expire"/> + <title value="Customer Session Expireon"/> + <description value="Customer Session Expire"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14110"/> + <group value="Backend"/> + <group value="mtf_migrated"/> + </annotations> + <after> + <!-- 6. Restore default configuration settings. --> + <magentoCLI command="config:set {{DefaultWebCookieLifetimeConfigData.path}} {{DefaultWebCookieLifetimeConfigData.value}}" stepKey="setDefaultCookieLifetime"/> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- 1. Login to Admin. --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- 2. Create customer if needed. --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- 3. Apply configuration settings. --> + <magentoCLI command="config:set {{ChangeWebCookieLifetimeConfigData.path}} {{ChangeWebCookieLifetimeConfigData.value}}" stepKey="changeCookieLifetime"/> + + <!-- 4. Wait for session to expire. --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <wait time="60" stepKey="waitForCookieLifetime"/> + <reloadPage stepKey="reloadPage"/> + + <!-- 5. Perform asserts. --> + <seeElement selector="{{StorefrontPanelHeaderSection.customerLoginLink}}" stepKey="assertAuthorizationLinkIsVisibleOnStoreFront"/> + </test> +</tests> 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 e62b73f39241d..51411ce04aac4 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 @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Test class for \Magento\Backend\Block\Widget\Grid\Massaction - */ namespace Magento\Backend\Test\Unit\Block\Widget\Grid; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; use Magento\Framework\Authorization; +use Magento\Framework\Data\Collection\AbstractDb as Collection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +/** + * Test class for \Magento\Backend\Block\Widget\Grid\Massaction + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class MassactionTest extends \PHPUnit\Framework\TestCase { /** @@ -54,6 +59,21 @@ class MassactionTest extends \PHPUnit\Framework\TestCase */ private $visibilityCheckerMock; + /** + * @var Collection|\PHPUnit\Framework\MockObject\MockObject + */ + private $gridCollectionMock; + + /** + * @var Select|\PHPUnit\Framework\MockObject\MockObject + */ + private $gridCollectionSelectMock; + + /** + * @var AdapterInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $connectionMock; + protected function setUp() { $this->_gridMock = $this->getMockBuilder(\Magento\Backend\Block\Widget\Grid::class) @@ -97,6 +117,18 @@ protected function setUp() ->setMethods(['isAllowed']) ->getMock(); + $this->gridCollectionMock = $this->createMock(Collection::class); + $this->gridCollectionSelectMock = $this->createMock(Select::class); + $this->connectionMock = $this->createMock(AdapterInterface::class); + + $this->gridCollectionMock->expects($this->any()) + ->method('getSelect') + ->willReturn($this->gridCollectionSelectMock); + + $this->gridCollectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $arguments = [ 'layout' => $this->_layoutMock, 'request' => $this->_requestMock, @@ -269,6 +301,41 @@ public function testGetGridIdsJsonWithoutUseSelectAll() $this->assertEmpty($this->_block->getGridIdsJson()); } + /** + * Test for getGridIdsJson when select all functionality flag set to true. + */ + public function testGetGridIdsJsonWithUseSelectAll() + { + $this->_block->setUseSelectAll(true); + + $this->_gridMock->expects($this->once()) + ->method('getCollection') + ->willReturn($this->gridCollectionMock); + + $this->gridCollectionSelectMock->expects($this->exactly(4)) + ->method('reset') + ->withConsecutive( + [Select::ORDER], + [Select::LIMIT_COUNT], + [Select::LIMIT_OFFSET], + [Select::COLUMNS] + ); + + $this->gridCollectionSelectMock->expects($this->once()) + ->method('columns') + ->with('test_id'); + + $this->connectionMock->expects($this->once()) + ->method('fetchCol') + ->with($this->gridCollectionSelectMock) + ->willReturn([1, 2, 3]); + + $this->assertEquals( + '1,2,3', + $this->_block->getGridIdsJson() + ); + } + /** * @param string $itemId * @param array|\Magento\Framework\DataObject $item diff --git a/app/code/Magento/Backend/Test/Unit/Helper/JsTest.php b/app/code/Magento/Backend/Test/Unit/Helper/JsTest.php new file mode 100644 index 0000000000000..ff10158a11943 --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Helper/JsTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Test\Unit\Helper; + +use Magento\Backend\Helper\Js; +use PHPUnit\Framework\TestCase; + +/** + * Class JsTest + * + * Testing decoding serialized grid data + */ +class JsTest extends TestCase +{ + /** + * @var Js + */ + private $helper; + + /** + * Set Up + */ + protected function setUp() + { + $this->helper = new Js(); + } + + /** + * Test decoding the serialized input + * + * @dataProvider getEncodedDataProvider + * + * @param string $encoded + * @param array $expected + */ + public function testDecodeGridSerializedInput(string $encoded, array $expected) + { + $this->assertEquals($expected, $this->helper->decodeGridSerializedInput($encoded)); + } + + /** + * Get serialized grid input + * + * @return array + */ + public function getEncodedDataProvider(): array + { + return [ + 'Decoding empty serialized string' => [ + '', + [] + ], + 'Decoding a simplified serialized string' => [ + '1&2&3&4', + [1, 2, 3, 4] + ], + 'Decoding encoded serialized string' => [ + '2=dGVzdC1zdHJpbmc=', + [ + 2 => [ + 'test-string' => '' + ] + ] + ], + 'Decoding multiple encoded serialized strings' => [ + '2=dGVzdC1zdHJpbmc=&3=bmV3LXN0cmluZw==', + [ + 2 => [ + 'test-string' => '' + ], + 3 => [ + 'new-string' => '' + ] + ] + ] + ]; + } +} 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 f1a4bc355b08e..dd8e06307cecc 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php @@ -18,38 +18,41 @@ class SessionTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Backend\App\Config | \PHPUnit_Framework_MockObject_MockObject */ - protected $config; + private $config; /** * @var \Magento\Framework\Session\Config | \PHPUnit_Framework_MockObject_MockObject */ - protected $sessionConfig; + private $sessionConfig; /** * @var \Magento\Framework\Stdlib\CookieManagerInterface | \PHPUnit_Framework_MockObject_MockObject */ - protected $cookieManager; + private $cookieManager; /** * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory | \PHPUnit_Framework_MockObject_MockObject */ - protected $cookieMetadataFactory; + private $cookieMetadataFactory; /** * @var \Magento\Framework\Session\Storage | \PHPUnit_Framework_MockObject_MockObject */ - protected $storage; + private $storage; /** * @var \Magento\Framework\Acl\Builder | \PHPUnit_Framework_MockObject_MockObject */ - protected $aclBuilder; + private $aclBuilder; /** * @var Session */ - protected $session; + private $session; + /** + * @inheritdoc + */ protected function setUp() { $this->cookieMetadataFactory = $this->createPartialMock( diff --git a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php index 77c428a6a116a..8264c0868eb90 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php @@ -5,21 +5,21 @@ */ namespace Magento\Backend\Test\Unit\Model\Authorization; -/** - * Class RoleLocatorTest - */ class RoleLocatorTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Backend\Model\Authorization\RoleLocator */ - protected $_model; + private $_model; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_sessionMock = []; + private $_sessionMock = []; + /** + * @inheritdoc + */ protected function setUp() { $this->_sessionMock = $this->createPartialMock( diff --git a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php index ce2b65a2249ac..f3d62b34c46e9 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php @@ -7,49 +7,49 @@ use Magento\Framework\Locale\Resolver; -/** - * Class ManagerTest - */ class ManagerTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Backend\Model\Locale\Manager */ - protected $_model; + private $_model; /** * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\TranslateInterface */ - protected $_translator; + private $_translator; /** * @var \Magento\Backend\Model\Session */ - protected $_session; + private $_session; /** * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\Model\Auth\Session */ - protected $_authSession; - + private $_authSession; + /** * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\App\ConfigInterface */ - protected $_backendConfig; + private $_backendConfig; + /** + * @inheritdoc + */ protected function setUp() { $this->_session = $this->createMock(\Magento\Backend\Model\Session::class); $this->_authSession = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['getUser']); - + $this->_backendConfig = $this->getMockForAbstractClass( \Magento\Backend\App\ConfigInterface::class, [], '', false ); - + $userMock = new \Magento\Framework\DataObject(); $this->_authSession->expects($this->any())->method('getUser')->will($this->returnValue($userMock)); diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php index cd3128754444b..5a4c8e978b78b 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php @@ -10,7 +10,7 @@ ' resource="Test_Value::value"/></menu></config>', [ "Element 'add', attribute 'action': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[a-zA-Z0-9/_]{3,}'.\nLine: 1\n", + "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n", "Element 'add', attribute 'action': '' is not a valid value of the atomic type 'typeAction'.\nLine: 1\n" ], ], @@ -20,7 +20,7 @@ 'resource="Test_Value::value"/></menu></config>', [ "Element 'add', attribute 'action': [facet 'pattern'] The value 'ad' is not accepted by the " . - "pattern '[a-zA-Z0-9/_]{3,}'.\nLine: 1\n", + "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n", "Element 'add', attribute 'action': 'ad' is not a valid value of the atomic type 'typeAction'.\nLine: 1\n" ], ], @@ -31,7 +31,7 @@ '</menu></config>', [ "Element 'add', attribute 'action': [facet 'pattern'] The value 'adm$#@inhtml/notification' is not " . - "accepted by the pattern '[a-zA-Z0-9/_]{3,}'.\nLine: 1\n", + "accepted by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n", "Element 'add', attribute 'action': 'adm$#@inhtml/notification' is not a valid value of the atomic " . "type 'typeAction'.\nLine: 1\n" ], @@ -452,7 +452,7 @@ '<?xml version="1.0"?><config><menu><update action="" ' . 'id="Test_Value::some_value"/></menu></config>', [ "Element 'update', attribute 'action': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[a-zA-Z0-9/_]{3,}'.\nLine: 1\n", + "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n", "Element 'update', attribute 'action': '' is not a valid value of the atomic type 'typeAction'.\nLine: 1\n" ], ], @@ -462,7 +462,7 @@ 'resource="Test_Value::value"/></menu></config>', [ "Element 'update', attribute 'action': [facet 'pattern'] The value 'v' is not accepted by the " . - "pattern '[a-zA-Z0-9/_]{3,}'.\nLine: 1\n", + "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n", "Element 'update', attribute 'action': 'v' is not a valid value of the atomic type 'typeAction'.\nLine: 1\n" ], ], @@ -471,7 +471,7 @@ 'id="Test_Value::some_value"/></menu></config>', [ "Element 'update', attribute 'action': [facet 'pattern'] The value '/@##gt;' is not " . - "accepted by the pattern '[a-zA-Z0-9/_]{3,}'.\nLine: 1\n", + "accepted by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n", "Element 'update', attribute 'action': '/@##gt;' is not a valid value of the atomic" . " type 'typeAction'.\nLine: 1\n" ], diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 343ecc0ee3d58..4a92ed8124bf8 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -323,7 +323,8 @@ </field> <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> + <validate>validate-digits validate-digits-range digits-range-0-65535</validate> + <comment>Please enter at least 0 and at most 65535 (For Windows server only).</comment> </field> <field id="set_return_path" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Set Return-Path</label> @@ -481,22 +482,22 @@ <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>Specify URL or {{base_url}} placeholder.</comment> + <comment><![CDATA[Specify URL or {{base_url}} placeholder.]]></comment> </field> <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> + <comment><![CDATA[May start with {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_static_url" translate="label comment" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URL for Static View Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May be empty or start with {{unsecure_base_url}} placeholder.]]></comment> </field> <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> + <comment><![CDATA[May be empty or start with {{unsecure_base_url}} placeholder.]]></comment> </field> </group> <group id="secure" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -505,22 +506,22 @@ <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Secure Base URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.]]></comment> </field> <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> + <comment><![CDATA[May start with {{secure_base_url}} or {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_static_url" translate="label comment" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Secure Base URL for Static View 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> + <comment><![CDATA[May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.]]></comment> </field> <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> + <comment><![CDATA[May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.]]></comment> </field> <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> @@ -585,11 +586,6 @@ <label>Validate HTTP_USER_AGENT</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="use_frontend_sid" translate="label comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Use SID on Storefront</label> - <comment>Allows customers to stay logged in when switching between different stores.</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> </group> </section> </system> diff --git a/app/code/Magento/Backend/etc/menu.xsd b/app/code/Magento/Backend/etc/menu.xsd index 2619b3f5fedac..4b408e8e86a17 100644 --- a/app/code/Magento/Backend/etc/menu.xsd +++ b/app/code/Magento/Backend/etc/menu.xsd @@ -100,7 +100,7 @@ </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z0-9/_]{3,}" /> + <xs:pattern value="[a-zA-Z0-9/_\-]{3,}" /> </xs:restriction> </xs:simpleType> diff --git a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js index ae0e84e2d27f8..e886f28cd158b 100644 --- a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - 'mediaUploader': 'Magento_Backend/js/media-uploader' + 'mediaUploader': 'Magento_Backend/js/media-uploader', + 'mage/translate': 'Magento_Backend/js/translate' } } }; 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 b712bc6c95315..7f6f2bbd13fa5 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -170,6 +170,9 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; <?php if ($block->getSortableUpdateCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.sortableUpdateCallback = <?= /* @noEscape */ $block->getSortableUpdateCallback() ?>; <?php endif; ?> + <?php if ($block->getFilterKeyPressCallback()) : ?> + <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; + <?php endif; ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.bindSortable(); <?php if ($block->getRowInitCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; 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 0bb453f25d7ca..527ddc436207f 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 @@ -272,6 +272,9 @@ $numColumns = count($block->getColumns()); <?php if ($block->getCheckboxCheckCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; <?php endif; ?> + <?php if ($block->getFilterKeyPressCallback()) : ?> + <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; + <?php endif; ?> <?php if ($block->getRowInitCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/translate.js b/app/code/Magento/Backend/view/adminhtml/web/js/translate.js new file mode 100644 index 0000000000000..d6e1547600c4e --- /dev/null +++ b/app/code/Magento/Backend/view/adminhtml/web/js/translate.js @@ -0,0 +1,48 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable strict */ +define([ + 'jquery', + 'mage/mage' +], function ($) { + $.extend(true, $, { + mage: { + translate: (function () { + /** + * Key-value translations storage + * @type {Object} + * @private + */ + var _data = {}; + + /** + * Add new translation (two string parameters) or several translations (object) + */ + this.add = function () { + if (arguments.length > 1) { + _data[arguments[0]] = arguments[1]; + } else if (typeof arguments[0] === 'object') { + $.extend(_data, arguments[0]); + } + }; + + /** + * Make a translation with parsing (to handle case when _data represents tuple) + * @param {String} text + * @return {String} + */ + this.translate = function (text) { + return _data[text] ? _data[text] : text; + }; + + return this; + }()) + } + }); + $.mage.__ = $.proxy($.mage.translate.translate, $.mage.translate); + + return $.mage.__; +}); diff --git a/app/code/Magento/Backup/README.md b/app/code/Magento/Backup/README.md index 59688ea3e716e..e1167bc4f2429 100644 --- a/app/code/Magento/Backup/README.md +++ b/app/code/Magento/Backup/README.md @@ -1,3 +1,28 @@ -The Backup module allows administrators to perform backups and rollbacks. Types of backups include system, database and media backups. This module relies on the Cron module to schedule backups. +# Magento_Backup module -This module does not affect the storefront. +The Magento_Backup module allows administrators to perform backups and rollbacks. Types of backups include system, database and media backups. This module relies on the Cron module to schedule backups. + +The Magento_Backup module does not affect the storefront. + +For more information about this module, see [Magento Backups](https://docs.magento.com/m2/ce/user_guide/system/backups.html) + +## Extensibility + +Extension developers can interact with the Magento_Backup module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backup module. + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +`backup_index_block` +`backup_index_disabled` +`backup_index_grid` +`backup_index_index` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml index aa6635b4dde4a..78e0aae1dd4c2 100644 --- a/app/code/Magento/Backup/etc/adminhtml/system.xml +++ b/app/code/Magento/Backup/etc/adminhtml/system.xml @@ -12,7 +12,7 @@ <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> + <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"> diff --git a/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php new file mode 100644 index 0000000000000..403c4d72fe358 --- /dev/null +++ b/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Braintree\Gateway\Request; + +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Braintree\Gateway\SubjectReader; + +/** + * Class BillingAddressDataBuilder + */ +class BillingAddressDataBuilder implements BuilderInterface +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * BillingAddress block name + */ + private const BILLING_ADDRESS = 'billing'; + + /** + * The customer’s company. 255 character maximum. + */ + private const COMPANY = 'company'; + + /** + * The first name value must be less than or equal to 255 characters. + */ + private const FIRST_NAME = 'firstName'; + + /** + * The last name value must be less than or equal to 255 characters. + */ + private const LAST_NAME = 'lastName'; + + /** + * The street address. Maximum 255 characters, and must contain at least 1 digit. + * Required when AVS rules are configured to require street address. + */ + private const STREET_ADDRESS = 'streetAddress'; + + /** + * The postal code. Postal code must be a string of 5 or 9 alphanumeric digits, + * optionally separated by a dash or a space. Spaces, hyphens, + * and all other special characters are ignored. + */ + private const POSTAL_CODE = 'postalCode'; + + /** + * The ISO 3166-1 alpha-2 country code specified in an address. + * The gateway only accepts specific alpha-2 values. + * + * @link https://developers.braintreepayments.com/reference/general/countries/php#list-of-countries + */ + private const COUNTRY_CODE = 'countryCodeAlpha2'; + + /** + * The extended address information—such as apartment or suite number. 255 character maximum. + */ + private const EXTENDED_ADDRESS = 'extendedAddress'; + + /** + * The locality/city. 255 character maximum. + */ + private const LOCALITY = 'locality'; + + /** + * The state or province. For PayPal addresses, the region must be a 2-letter abbreviation; + */ + private const REGION = 'region'; + + /** + * @param SubjectReader $subjectReader + */ + public function __construct(SubjectReader $subjectReader) + { + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + + $result = []; + $order = $paymentDO->getOrder(); + + $billingAddress = $order->getBillingAddress(); + if ($billingAddress) { + $result[self::BILLING_ADDRESS] = [ + self::REGION => $billingAddress->getRegionCode(), + self::POSTAL_CODE => $billingAddress->getPostcode(), + self::COUNTRY_CODE => $billingAddress->getCountryId(), + self::FIRST_NAME => $billingAddress->getFirstname(), + self::STREET_ADDRESS => $billingAddress->getStreetLine1(), + self::LAST_NAME => $billingAddress->getLastname(), + self::COMPANY => $billingAddress->getCompany(), + self::EXTENDED_ADDRESS => $billingAddress->getStreetLine2(), + self::LOCALITY => $billingAddress->getCity() + ]; + } + + return $result; + } +} diff --git a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php index 58ce33305da85..2f73dd8f380dc 100644 --- a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php +++ b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php @@ -11,6 +11,7 @@ use Braintree\Error\Validation; use Braintree\Result\Error; use Braintree\Result\Successful; +use Braintree\Transaction; /** * Processes errors codes from Braintree response. @@ -38,12 +39,14 @@ public function getErrorCodes($response): array $result[] = $error->code; } - if (isset($response->transaction) && $response->transaction->status === 'gateway_rejected') { - $result[] = $response->transaction->gatewayRejectionReason; - } + if (isset($response->transaction) && $response->transaction) { + if ($response->transaction->status === Transaction::GATEWAY_REJECTED) { + $result[] = $response->transaction->gatewayRejectionReason; + } - if (isset($response->transaction) && $response->transaction->status === 'processor_declined') { - $result[] = $response->transaction->processorResponseCode; + if ($response->transaction->status === Transaction::PROCESSOR_DECLINED) { + $result[] = $response->transaction->processorResponseCode; + } } return $result; diff --git a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php index ab23037b4e98e..1ba696839a95d 100644 --- a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php @@ -67,11 +67,12 @@ public function __construct( public function getConfig() { $storeId = $this->session->getStoreId(); + $isActive = $this->config->isActive($storeId); return [ 'payment' => [ self::CODE => [ - 'isActive' => $this->config->isActive($storeId), - 'clientToken' => $this->getClientToken(), + 'isActive' => $isActive, + 'clientToken' => $isActive ? $this->getClientToken() : null, 'ccTypesMapper' => $this->config->getCcTypesMapper(), 'sdkUrl' => $this->config->getSdkUrl(), 'hostedFieldsSdkUrl' => $this->config->getHostedFieldsSdkUrl(), diff --git a/app/code/Magento/Braintree/README.md b/app/code/Magento/Braintree/README.md index 8c34b7ae1af67..66d872e55a21a 100644 --- a/app/code/Magento/Braintree/README.md +++ b/app/code/Magento/Braintree/README.md @@ -1 +1,47 @@ -Module Magento\Braintree implements integration with the Braintree payment system. \ No newline at end of file +# Magento_Braintree module + +The Magento_Braintree module implements integration with the Braintree payment system. + +## Extensibility + +Extension developers can interact with the Magento_Braintree module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Braintree module. + +### Events + +This module observes the following events: + + - `payment_method_assign_data_braintree` event in `Magento\Braintree\Observer\DataAssignObserver` file. + - `payment_method_assign_data_braintree_paypal` event in `Magento\Braintree\Observer\DataAssignObserver` file. + - `shortcut_buttons_container` event in `Magento\Braintree\Observer\AddPaypalShortcuts` file. + +For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +This module interacts with the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `braintree_paypal_review` +- `checkout_index_index` +- `multishipping_checkout_billing` +- `vault_cards_listaction` + +This module interacts with the following layout handles in the `view/frontend/layout` directory: + +- `adminhtml_system_config_edit` +- `braintree_report_index` +- `sales_order_create_index` +- `sales_order_create_load_block_billing_method` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +### UI components + +You can extend admin notifications using the `view/adminhtml/ui_component/braintree_report.xml` configuration file. + +For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php index cddb4852da0e3..605e9253fe2cc 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php @@ -74,9 +74,9 @@ public function getErrorCodeDataProvider(): array 'errors' => [], 'transaction' => [ 'status' => 'processor_declined', - 'processorResponseCode' => '1000' + 'processorResponseCode' => '2059' ], - 'expectedResult' => ['1000'] + 'expectedResult' => ['2059'] ], [ 'errors' => [ 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 55bc2cb195d6e..fb34113be15ae 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/ConfigProviderTest.php @@ -76,7 +76,7 @@ protected function setUp() } /** - * Run test getConfig method + * Ensure that get config returns correct data if payment is active or not * * @param array $config * @param array $expected @@ -84,22 +84,37 @@ protected function setUp() */ public function testGetConfig($config, $expected) { - $this->braintreeAdapter->expects(static::once()) - ->method('generate') - ->willReturn(self::CLIENT_TOKEN); + if ($config['isActive']) { + $this->braintreeAdapter->expects($this->once()) + ->method('generate') + ->willReturn(self::CLIENT_TOKEN); + } else { + $config = array_replace_recursive( + $this->getConfigDataProvider()[0]['config'], + $config + ); + $expected = array_replace_recursive( + $this->getConfigDataProvider()[0]['expected'], + $expected + ); + $this->braintreeAdapter->expects($this->never()) + ->method('generate'); + } foreach ($config as $method => $value) { - $this->config->expects(static::once()) + $this->config->expects($this->once()) ->method($method) ->willReturn($value); } - static::assertEquals($expected, $this->configProvider->getConfig()); + $this->assertEquals($expected, $this->configProvider->getConfig()); } /** - * @covers \Magento\Braintree\Model\Ui\ConfigProvider::getClientToken + * @covers \Magento\Braintree\Model\Ui\ConfigProvider::getClientToken * @dataProvider getClientTokenDataProvider + * @param $merchantAccountId + * @param $params */ public function testGetClientToken($merchantAccountId, $params) { @@ -124,7 +139,7 @@ public function getConfigDataProvider() [ 'config' => [ 'isActive' => true, - 'getCcTypesMapper' => ['visa' => 'VI', 'american-express'=> 'AE'], + 'getCcTypesMapper' => ['visa' => 'VI', 'american-express' => 'AE'], 'getSdkUrl' => self::SDK_URL, 'getHostedFieldsSdkUrl' => 'https://sdk.com/test.js', 'getCountrySpecificCardTypeConfig' => [ @@ -148,7 +163,7 @@ public function getConfigDataProvider() 'ccTypesMapper' => ['visa' => 'VI', 'american-express' => 'AE'], 'sdkUrl' => self::SDK_URL, 'hostedFieldsSdkUrl' => 'https://sdk.com/test.js', - 'countrySpecificCardTypes' =>[ + 'countrySpecificCardTypes' => [ 'GB' => ['VI', 'AE'], 'US' => ['DI', 'JCB'] ], @@ -166,6 +181,19 @@ public function getConfigDataProvider() ] ] ] + ], + [ + 'config' => [ + 'isActive' => false, + ], + 'expected' => [ + 'payment' => [ + ConfigProvider::CODE => [ + 'isActive' => false, + 'clientToken' => null, + ] + ] + ] ] ]; } diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 5b5eeaf2b3dd7..58049f7bf0f93 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -22,11 +22,11 @@ "magento/module-sales": "*", "magento/module-ui": "*", "magento/module-vault": "*", - "magento/module-multishipping": "*" + "magento/module-multishipping": "*", + "magento/module-theme": "*" }, "suggest": { - "magento/module-checkout-agreements": "*", - "magento/module-theme": "*" + "magento/module-checkout-agreements": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml index 7155264b4e6ad..bffcc75705938 100644 --- a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml +++ b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml @@ -21,6 +21,7 @@ <message code="81723" translate="true">Cardholder name is too long.</message> <message code="81736" translate="true">CVV verification failed.</message> <message code="cvv" translate="true">CVV verification failed.</message> + <message code="2059" translate="true">Address 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> diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index f6ad552fc9bef..ef12b5eae6835 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -381,7 +381,7 @@ <item name="customer" xsi:type="string">Magento\Braintree\Gateway\Request\CustomerDataBuilder</item> <item name="payment" xsi:type="string">Magento\Braintree\Gateway\Request\PaymentDataBuilder</item> <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="address" xsi:type="string">Magento\Braintree\Gateway\Request\BillingAddressDataBuilder</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> 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 ac97e4fa5eb58..5f06d26e2acfc 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 @@ -246,8 +246,10 @@ define( return; } - self.setPaymentPayload(payload); - self.placeOrder(); + if (self.validateCardType()) { + self.setPaymentPayload(payload); + self.placeOrder(); + } }); } }, 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 ae9f69c405c2b..ea5200e4ba51f 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 @@ -17,7 +17,8 @@ define([ 'Magento_Vault/js/view/payment/vault-enabler', 'Magento_Checkout/js/action/create-billing-address', 'Magento_Braintree/js/view/payment/kount', - 'mage/translate' + 'mage/translate', + 'Magento_Ui/js/model/messageList' ], function ( $, _, @@ -31,7 +32,8 @@ define([ VaultEnabler, createBillingAddress, kount, - $t + $t, + globalMessageList ) { 'use strict'; @@ -334,7 +336,7 @@ define([ } return { - line1: address.street[0], + line1: _.isUndefined(address.street) || _.isUndefined(address.street[0]) ? '' : address.street[0], city: address.city, state: address.regionCode, postalCode: address.postcode, @@ -415,6 +417,18 @@ define([ */ onVaultPaymentTokenEnablerChange: function () { this.reInitPayPal(); + }, + + /** + * Show error message + * + * @param {String} errorMessage + * @private + */ + showError: function (errorMessage) { + globalMessageList.addErrorMessage({ + message: errorMessage + }); } }); }); 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 9bcb5dad8b636..8da8927a3b247 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 @@ -23,7 +23,7 @@ <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> </div> - <form id="co-transparent-form-braintree" class="form" data-bind="" method="post" action="#" novalidate="novalidate"> + <form id="co-transparent-form-braintree" class="form" data-bind="afterRender: initHostedFields" method="post" action="#" novalidate="novalidate"> <fieldset data-bind="attr: {class: 'fieldset payment items ccard ' + getCode(), id: 'payment_form_' + getCode()}"> <legend class="legend"> <span><!-- ko i18n: 'Credit Card Information'--><!-- /ko --></span> @@ -87,7 +87,7 @@ <span><!-- ko i18n: 'Card Verification Number'--><!-- /ko --></span> </label> <div class="control _with-tooltip"> - <div data-bind="afterRender: initHostedFields, attr: {id: getCode() + '_cc_cid'}" class="hosted-control hosted-cid"></div> + <div data-bind="attr: {id: getCode() + '_cc_cid'}" class="hosted-control hosted-cid"></div> <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Card Verification Number'--><!-- /ko --></div> <div class="field-tooltip toggle"> diff --git a/app/code/Magento/BraintreeGraphQl/README.md b/app/code/Magento/BraintreeGraphQl/README.md index f6740e4d250e9..4e8eecc93a924 100644 --- a/app/code/Magento/BraintreeGraphQl/README.md +++ b/app/code/Magento/BraintreeGraphQl/README.md @@ -1,4 +1,9 @@ -# BraintreeGraphQl +# Magento_BraintreeGraphQl module -**BraintreeGraphQl** provides type and resolver for method additional -information. \ No newline at end of file +The Magento_BraintreeGraphQl module provides type and resolver information for the GraphQL module to pass payment information data from the client to Magento. + +## Extensibility + +Extension developers can interact with the Magento_BraintreeGraphQl module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_BraintreeGraphQl module. diff --git a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls index 0492f8aaf989b..08bd10fd4c2dd 100644 --- a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls @@ -2,7 +2,7 @@ # See COPYING.txt for license details. type Mutation { - createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Braintree Client Token for creating client-side nonce.") + createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Client Token for Braintree Javascript SDK initialization.") } input PaymentMethodInput { @@ -11,9 +11,9 @@ input PaymentMethodInput { } input BraintreeInput { - payment_method_nonce: String! - is_active_payment_token_enabler: Boolean! - device_data: String + payment_method_nonce: String! @doc(description:"The one-time payment token generated by Braintree payment gateway based on card details. Required field to make sale transaction.") + is_active_payment_token_enabler: Boolean! @doc(description:"States whether an entered by a customer credit/debit card should be tokenized for later usage. Required only if Vault is enabled for Braintree payment integration.") + device_data: String @doc(description:"Contains a fingerprint provided by Braintree JS SDK and should be sent with sale transaction details to the Braintree payment gateway. Should be specified only in a case if Kount (advanced fraud protection) is enabled for Braintree payment integration.") } input BraintreeCcVaultInput { diff --git a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php index c0a2d9d43034d..863f273225693 100644 --- a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php +++ b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php @@ -32,7 +32,7 @@ class Renderer extends \Magento\Checkout\Block\Cart\Item\Renderer * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param PriceCurrencyInterface $priceCurrency - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param InterpretationStrategyInterface $messageInterpretationStrategy * @param Configuration $bundleProductConfiguration * @param array $data @@ -46,7 +46,7 @@ public function __construct( \Magento\Framework\Url\Helper\Data $urlHelper, \Magento\Framework\Message\ManagerInterface $messageManager, PriceCurrencyInterface $priceCurrency, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, InterpretationStrategyInterface $messageInterpretationStrategy, Configuration $bundleProductConfiguration, array $data = [] diff --git a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php index 61559df4d2cf6..ecbf4cc80a3ae 100644 --- a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php +++ b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type; +/** + * Provides duplicating bundle options and selections + */ class Bundle implements \Magento\Catalog\Model\Product\CopyConstructorInterface { /** @@ -27,7 +30,17 @@ public function build(Product $product, Product $duplicate) $bundleOptions = $product->getExtensionAttributes()->getBundleProductOptions() ?: []; $duplicatedBundleOptions = []; foreach ($bundleOptions as $key => $bundleOption) { - $duplicatedBundleOptions[$key] = clone $bundleOption; + $duplicatedBundleOption = clone $bundleOption; + /** + * Set option and selection ids to 'null' in order to create new option(selection) for duplicated product, + * but not modifying existing one, which led to lost of option(selection) in original product. + */ + $productLinks = $duplicatedBundleOption->getProductLinks() ?: []; + foreach ($productLinks as $productLink) { + $productLink->setSelectionId(null); + } + $duplicatedBundleOption->setOptionId(null); + $duplicatedBundleOptions[$key] = $duplicatedBundleOption; } $duplicate->getExtensionAttributes()->setBundleProductOptions($duplicatedBundleOptions); } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index b71853cde41ac..077ebd4422aab 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -85,7 +85,7 @@ class Price implements DimensionalIndexerInterface private $eventManager; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManager; @@ -97,7 +97,7 @@ class Price implements DimensionalIndexerInterface * @param BasePriceModifier $basePriceModifier * @param JoinAttributeProcessor $joinAttributeProcessor * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param bool $fullReindexAction * @param string $connectionName * @@ -111,7 +111,7 @@ public function __construct( BasePriceModifier $basePriceModifier, JoinAttributeProcessor $joinAttributeProcessor, \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, $fullReindexAction = false, $connectionName = 'indexer' ) { @@ -139,16 +139,16 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) $temporaryPriceTable = $this->indexTableStructureFactory->create( [ - 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions), - 'entityField' => 'entity_id', - 'customerGroupField' => 'customer_group_id', - 'websiteField' => 'website_id', - 'taxClassField' => 'tax_class_id', - 'originalPriceField' => 'price', - 'finalPriceField' => 'final_price', - 'minPriceField' => 'min_price', - 'maxPriceField' => 'max_price', - 'tierPriceField' => 'tier_price', + 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions), + 'entityField' => 'entity_id', + 'customerGroupField' => 'customer_group_id', + 'websiteField' => 'website_id', + 'taxClassField' => 'tax_class_id', + 'originalPriceField' => 'price', + 'finalPriceField' => 'final_price', + 'minPriceField' => 'min_price', + 'maxPriceField' => 'max_price', + 'tierPriceField' => 'tier_price', ] ); @@ -335,9 +335,9 @@ private function prepareBundlePriceByType($priceType, array $dimensions, $entity ); $finalPrice = $connection->getLeastSql( [ - $price, - $connection->getIfNullSql($specialPriceExpr, $price), - $connection->getIfNullSql($tierPrice, $price), + $price, + $connection->getIfNullSql($specialPriceExpr, $price), + $connection->getIfNullSql($tierPrice, $price), ] ); } else { @@ -477,8 +477,8 @@ private function calculateBundleSelectionPrice($dimensions, $priceType) $priceExpr = $connection->getLeastSql( [ - $priceExpr, - $connection->getIfNullSql($tierExpr, $priceExpr), + $priceExpr, + $connection->getIfNullSql($tierExpr, $priceExpr), ] ); } else { @@ -495,8 +495,8 @@ private function calculateBundleSelectionPrice($dimensions, $priceType) ); $priceExpr = $connection->getLeastSql( [ - $specialExpr, - $connection->getIfNullSql($tierExpr, $price), + $specialExpr, + $connection->getIfNullSql($tierExpr, $price), ] ); } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index 21ba1f75ba90b..7b3f6dd8bbefa 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -61,7 +61,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -88,7 +88,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index c6a67cc5a110c..0000000000000 --- a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Bundle\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tables = [ - 'catalog_product_index_price_bundle_tmp', - 'catalog_product_index_price_bundle_sel_tmp', - 'catalog_product_index_price_bundle_opt_tmp', - ]; - foreach ($tables as $table) { - $tableName = $this->schemaSetup->getTable($table); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml index 2a50c5141ad4e..33740c346155a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml @@ -146,4 +146,17 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '4')}}" userInput="2" stepKey="fillQuantity5" after="fillQuantity4"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '5')}}" userInput="2" stepKey="fillQuantity6" after="fillQuantity5"/> </actionGroup> + + <actionGroup name="deleteBundleOptionByIndex"> + <annotations> + <description>Requires Navigation to Product Creation page. Removes any Bundle Option by index specified in arguments. 'deleteIndex' refers to Bundle option number.</description> + </annotations> + <arguments> + <argument name="deleteIndex" type="string"/> + </arguments> + + <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="conditionallyOpenSectionBundleItems"/> + <scrollTo selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" stepKey="scrollUpABit"/> + <click selector="{{AdminProductFormBundleSection.deleteOption(deleteIndex)}}" stepKey="clickDeleteOption"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index 6e7e4a7a16573..e5f557dd22ded 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -61,6 +61,20 @@ <requiredEntity type="custom_attribute">CustomAttributeDynamicPrice</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> </entity> + <entity name="ApiBundleProductUnderscoredSku" type="product2"> + <data key="name" unique="suffix">Api Bundle Product</data> + <data key="sku" unique="suffix">api_bundle_product</data> + <data key="type_id">bundle</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-bundle-product</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeDynamicPrice</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> + </entity> <entity name="ApiBundleProductPriceViewRange" type="product2"> <data key="name" unique="suffix">Api Bundle Product</data> <data key="sku" unique="suffix">api-bundle-product</data> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index bd13f4daa0dbd..967cf5ac49ed5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -46,6 +46,10 @@ <element name="currentBundleOption" type="text" selector="//div[@data-index='bundle-items']//div[contains(@class, 'admin__collapsible-title')]/span"/> <!--AddingAnOption--> <element name="addOptions" type="button" selector="//tr[@data-repeat-index='0']//td[4]" timeout="30"/> + <!--DragAnOption --> + <element name="dragOption" type="block" selector="//tr[{{dragIndex}}]//div[contains(@class, 'draggable-handle')]" timeout="30" parameterized="true"/> + <!--DeleteAnOption --> + <element name="deleteOption" type="button" selector="//tr[{{deleteIndex}}]//button[@data-index='delete_button']" timeout="30" parameterized="true"/> <!--SEODropdownTab--> <element name="seoDropdown" type="button" selector="//div[@data-index='search-engine-optimization']"/> <element name="seoDependent" type="button" selector="//div[@data-index='search-engine-optimization']//div[contains(@class, '_show')]"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml new file mode 100644 index 0000000000000..505a319c5c44f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.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="AdminAssociateBundleProductToWebsitesTest"> + <annotations> + <features value="Bundle"/> + <stories value="Create/Edit bundle product in Admin"/> + <title value="Admin should be able to associate bundle product to websites"/> + <description value="Admin should be able to associate bundle product to websites"/> + <testCaseId value="MC-3344"/> + <severity value="CRITICAL"/> + <group value="bundle"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Configure Store URLs --> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToYes"/> + + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create Simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + + <!-- Create Bundle product --> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createNewBundleLink"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + + <!-- Reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + </before> + <after> + <!-- Disabled Store URLs --> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToNo"/> + + <!-- Delete simple product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!-- Delete bundle product --> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + + <!-- Delete second website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + + <actionGroup ref="NavigateToAndResetProductGridToDefaultView" stepKey="resetProductGridFilter"/> + + <!-- Admin logout --> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Open product page and assign grouped project to second website --> + <actionGroup ref="filterAndSelectProduct" stepKey="openAdminProductPage"> + <argument name="productSku" value="$$createBundleProduct.sku$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductInWebsiteActionGroup" stepKey="assignProductToSecondWebsite"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="AdminUnassignProductInWebsiteActionGroup" stepKey="unassignProductFromDefaultWebsite"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveGroupedProduct"/> + + <!-- Assert product is assigned to Second website --> + <actionGroup ref="AssertProductIsAssignedToWebsite" stepKey="seeCustomWebsiteIsChecked"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + + <!-- Assert product is not assigned to Main website --> + <actionGroup ref="AssertProductIsNotAssignedToWebsite" stepKey="seeMainWebsiteIsNotChecked"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + </actionGroup> + + <!-- Go to frontend and open product on Main website --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Assert 404 page --> + <actionGroup ref="StorefrontAssertPageNotFoundErrorOnProductDetailPageActionGroup" stepKey="assertPageNotFoundError"> + <argument name="product" value="$$createBundleProduct$$"/> + </actionGroup> + + <!-- Assert product is present at Second website --> + <actionGroup ref="StorefrontOpenProductPageUsingStoreCodeInUrlActionGroup" stepKey="openProductPageUsingStoreCodeInUrl"> + <argument name="product" value="$$createBundleProduct$$"/> + <argument name="storeView" value="SecondStoreUnique"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml index f7a64f943f307..42584a31651d7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml @@ -32,6 +32,11 @@ <!-- Delete the simple product --> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!-- Delete a Website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + <!-- Log out --> <actionGroup ref="logout" stepKey="logout"/> </after> @@ -135,10 +140,116 @@ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> + </test> + <test name="AdminCreateAndEditBundleProductOptionsNegativeTest"> + <annotations> + <features value="Bundle"/> + <stories value="Modify bundle product in Admin"/> + <title value="Admin should be able to remove any bundle option a bundle product"/> + <description value="Admin should be able to set/edit other product information when creating/editing a bundle product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-224"/> + <skip> + <issueId value="https://github.com/magento/magento2/issues/25468"/> + </skip> + <group value="Catalog"/> + </annotations> + <before> + <!-- Create a Website --> + <createData entity="customWebsite" stepKey="createWebsite"/> + + <!-- Create first simple product for a bundle option --> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> - <!-- Delete created Website --> - <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> - <argument name="websiteName" value="$createWebsite.website[name]$"/> + <!-- Create second simple product for a bundle option --> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete the simple product --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + + <!-- Delete the simple product --> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + + <!-- Delete a Website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create new bundle product --> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createBundleProduct"> + <argument name="productType" value="bundle"/> + </actionGroup> + + <!-- Fill all main fields --> + <actionGroup ref="fillMainBundleProductForm" stepKey="fillMainProductFields"/> + + <!-- Add first bundle option to the product --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addFirstBundleOption"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="prodTwoSku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="optionTitle" value="{{RadioButtonsOption.title}}"/> + <argument name="inputType" value="{{RadioButtonsOption.type}}"/> + </actionGroup> + + <!-- Add second bundle option to the product --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addSecondBundleOption"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="prodTwoSku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="optionTitle" value="{{CheckboxOption.title}}"/> + <argument name="inputType" value="{{CheckboxOption.type}}"/> + </actionGroup> + + <!-- Add third bundle option to the product --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addThirdBundleOption"> + <argument name="x" value="2"/> + <argument name="n" value="3"/> + <argument name="prodOneSku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="prodTwoSku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="optionTitle" value="{{RadioButtonsOption.title}}"/> + <argument name="inputType" value="{{RadioButtonsOption.type}}"/> + </actionGroup> + + <!-- Set product in created Website --> + <actionGroup ref="AdminAssignProductInWebsiteActionGroup" stepKey="selectProductInWebsites"> + <argument name="website" value="$createWebsite.website[name]$"/> + </actionGroup> + + <!-- Save product form --> + <actionGroup ref="saveProductForm" stepKey="saveWithThreeOptions"/> + + <!-- Open created product --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!-- Remove second option --> + <actionGroup ref="deleteBundleOptionByIndex" stepKey="deleteSecondOption"> + <argument name="deleteIndex" value="1"/> + </actionGroup> + + <!-- Save product form --> + <actionGroup ref="saveProductForm" stepKey="clickSaveProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveWithTwoOptions"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> + + <!-- Delete created bundle product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="BundleProduct"/> </actionGroup> </test> </tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml index 1438958b92b61..730df90b31be6 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml @@ -24,6 +24,10 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> </before> <after> + <!-- Delete the bundled product we created in the test body --> + <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml index 52bce67600888..c6aab0ea54ea2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml @@ -17,6 +17,47 @@ <severity value="MAJOR"/> <testCaseId value="MC-139"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByNameMysqlTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product name using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product name using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20472"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -56,7 +97,7 @@ <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> - <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="ApiBundleProductUnderscoredSku" stepKey="product"/> <createData entity="DropDownBundleOption" stepKey="bundleOption"> <requiredEntity createDataKey="product"/> </createData> @@ -87,6 +128,47 @@ <severity value="MAJOR"/> <testCaseId value="MC-242"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product description using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20473"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -122,6 +204,47 @@ <severity value="MAJOR"/> <testCaseId value="MC-250"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByShortDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product short description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product short description using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20474"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -157,6 +280,56 @@ <severity value="MAJOR"/> <testCaseId value="MC-251"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <getData entity="GetProduct" stepKey="arg1"> + <requiredEntity createDataKey="product"/> + </getData> + <getData entity="GetProduct" stepKey="arg2"> + <requiredEntity createDataKey="simple1"/> + </getData> + <getData entity="GetProduct" stepKey="arg3"> + <requiredEntity createDataKey="simple2"/> + </getData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByPriceMysqlTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product price using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product price the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20475"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..d8d6034cd1a21 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml @@ -0,0 +1,47 @@ +<?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="StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types "/> + <title value="Guest customer should be able to advance search Bundle product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search Bundle product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20359"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml index 9ad4b6828d6e4..88db5b64fa42d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml @@ -76,6 +76,10 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Go to category page--> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> <waitForPageLoad stepKey="waitForHomePageToload"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml index d27cd0df88239..9ea0480e540ba 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml @@ -39,7 +39,7 @@ <requiredEntity createDataKey="fixedBundleOption"/> <requiredEntity createDataKey="createSimpleProductTwo"/> </createData> - <magentoCLI command="indexer:reindex" arguments="cataloginventory_stock catalogsearch_fulltext" stepKey="reindex"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> </before> <after> <deleteData createDataKey="createDynamicBundle" stepKey="deleteDynamicBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml new file mode 100644 index 0000000000000..18316e41241e4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -0,0 +1,197 @@ +<?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="StorefrontSortBundleProductsByPriceTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle products list on Storefront"/> + <title value="Customer should be able to sort bundle products by price when viewing products list"/> + <description value="Customer should be able to sort bundle products by price when viewing products list"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-228"/> + <group value="bundle"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create simple products for first bundle product --> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + + <!-- Create first bundle product --> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createFirstBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="firstProductBundleOption"> + <requiredEntity createDataKey="createFirstBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createFirstBundleLink"> + <requiredEntity createDataKey="createFirstBundleProduct"/> + <requiredEntity createDataKey="firstProductBundleOption"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createSecondBundleLink"> + <requiredEntity createDataKey="createFirstBundleProduct"/> + <requiredEntity createDataKey="firstProductBundleOption"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + + <!-- Create simple products for second bundle product --> + <createData entity="SimpleProduct2" stepKey="createFirstProduct"> + <field key="price">10.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createSecondProduct"/> + + <!-- Create second bundle product --> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createSecondBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="secondProductBundleOption"> + <requiredEntity createDataKey="createSecondBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLinkFirst"> + <requiredEntity createDataKey="createSecondBundleProduct"/> + <requiredEntity createDataKey="secondProductBundleOption"/> + <requiredEntity createDataKey="createFirstProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLinkSecond"> + <requiredEntity createDataKey="createSecondBundleProduct"/> + <requiredEntity createDataKey="secondProductBundleOption"/> + <requiredEntity createDataKey="createSecondProduct"/> + </createData> + + <!-- Create simple products for third bundle product --> + <createData entity="SimpleProduct2" stepKey="createFirstProductForBundle"/> + <createData entity="SimpleProduct2" stepKey="createSecondProductForBundle"> + <field key="price">500.00</field> + </createData> + + <!-- Create third bundle product --> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createThirdBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createThirdProductBundleOption"> + <requiredEntity createDataKey="createThirdBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleFirstLink"> + <requiredEntity createDataKey="createThirdBundleProduct"/> + <requiredEntity createDataKey="createThirdProductBundleOption"/> + <requiredEntity createDataKey="createFirstProductForBundle"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleSecondLink"> + <requiredEntity createDataKey="createThirdBundleProduct"/> + <requiredEntity createDataKey="createThirdProductBundleOption"/> + <requiredEntity createDataKey="createSecondProductForBundle"/> + </createData> + + <!-- Perform CLI reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <!-- Delete all created data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createFirstBundleProduct" stepKey="deleteFirstBundleProduct"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createSecondBundleProduct" stepKey="deleteSecondBundleProduct"/> + <deleteData createDataKey="createFirstProductForBundle" stepKey="deleteFirstProductForBundle"/> + <deleteData createDataKey="createSecondProductForBundle" stepKey="deleteSecondProductForBundle"/> + <deleteData createDataKey="createThirdBundleProduct" stepKey="deleteThirdBundleProduct"/> + </after> + + <!-- Open created category on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + + <!-- Assert first bundle products in category product grid --> + <actionGroup ref="AssertProductOnCategoryPageActionGroup" stepKey="assertFirstBundleProduct"> + <argument name="product" value="$$createFirstBundleProduct$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeFromForFirstBundleProduct"> + <argument name="selector" value="{{StorefrontCategoryProductSection.priceFromByProductId($$createFirstBundleProduct.id$$)}}"/> + <argument name="userInput" value="From $100.00"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeToForFirstBundleProduct"> + <argument name="selector" value="{{StorefrontCategoryProductSection.priceToByProductId($$createFirstBundleProduct.id$$)}}"/> + <argument name="userInput" value="To $123.00"/> + </actionGroup> + + <!-- Assert second bundle products in category product grid --> + <actionGroup ref="AssertProductOnCategoryPageActionGroup" stepKey="assertSecondBundleProduct"> + <argument name="product" value="$$createSecondBundleProduct$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeFromForSecondBundleProduct"> + <argument name="selector" value="{{StorefrontCategoryProductSection.priceFromByProductId($$createSecondBundleProduct.id$$)}}"/> + <argument name="userInput" value="From $10.00"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeToForSecondBundleProduct"> + <argument name="selector" value="{{StorefrontCategoryProductSection.priceToByProductId($$createSecondBundleProduct.id$$)}}"/> + <argument name="userInput" value="To $123.00"/> + </actionGroup> + + <!-- Assert third bundle products in category product grid --> + <actionGroup ref="AssertProductOnCategoryPageActionGroup" stepKey="assertThirdBundleProduct"> + <argument name="product" value="$$createThirdBundleProduct$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeFromForThirdBundleProduct"> + <argument name="selector" value="{{StorefrontCategoryProductSection.priceFromByProductId($$createThirdBundleProduct.id$$)}}"/> + <argument name="userInput" value="From $123.00"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seePriceRangeToForThirdBundleProduct"> + <argument name="selector" value="{{StorefrontCategoryProductSection.priceToByProductId($$createThirdBundleProduct.id$$)}}"/> + <argument name="userInput" value="To $500.00"/> + </actionGroup> + + <!-- Switch category view to List mode --> + <actionGroup ref="StorefrontSwitchCategoryViewToListMode" stepKey="switchCategoryViewToListMode"/> + + <!-- Sort products By Price --> + <actionGroup ref="StorefrontCategoryPageSortProductActionGroup" stepKey="sortProductByPrice"/> + <!-- Set Ascending Direction --> + <actionGroup ref="StorefrontCategoryPageSortAscendingActionGroup" stepKey="setAscendingDirection"/> + + <!-- Assert new products positions --> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductFirstPosition"> + <argument name="selector" value="{{StorefrontCategoryMainSection.lineProductName('1')}}"/> + <argument name="userInput" value="$$createThirdBundleProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductSecondPosition"> + <argument name="selector" value="{{StorefrontCategoryMainSection.lineProductName('2')}}"/> + <argument name="userInput" value="$$createFirstBundleProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductThirdPosition"> + <argument name="selector" value="{{StorefrontCategoryMainSection.lineProductName('3')}}"/> + <argument name="userInput" value="$$createSecondBundleProduct.name$$"/> + </actionGroup> + + <!-- Set Descending Direction --> + <actionGroup ref="StorefrontCategoryPageSortDescendingActionGroup" stepKey="setDescendingDirection"/> + + <!-- Assert new products positions --> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductNewFirstPosition"> + <argument name="selector" value="{{StorefrontCategoryMainSection.lineProductName('1')}}"/> + <argument name="userInput" value="$$createSecondBundleProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductNewSecondPosition"> + <argument name="selector" value="{{StorefrontCategoryMainSection.lineProductName('2')}}"/> + <argument name="userInput" value="$$createFirstBundleProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductNewThirdPosition"> + <argument name="selector" value="{{StorefrontCategoryMainSection.lineProductName('3')}}"/> + <argument name="userInput" value="$$createThirdBundleProduct.name$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php index 831098cc44c38..4df60d07d98ef 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php @@ -6,6 +6,7 @@ namespace Magento\Bundle\Test\Unit\Model\Product\CopyConstructor; use Magento\Bundle\Api\Data\BundleOptionInterface; +use Magento\Bundle\Model\Link; use Magento\Bundle\Model\Product\CopyConstructor\Bundle; use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; @@ -45,6 +46,7 @@ public function testBuildNegative() */ public function testBuildPositive() { + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); @@ -60,18 +62,42 @@ public function testBuildPositive() ->method('getExtensionAttributes') ->willReturn($extensionAttributesProduct); + $productLink = $this->getMockBuilder(Link::class) + ->setMethods(['setSelectionId']) + ->disableOriginalConstructor() + ->getMock(); + $productLink->expects($this->exactly(2)) + ->method('setSelectionId') + ->with($this->identicalTo(null)); + $firstOption = $this->getMockBuilder(BundleOptionInterface::class) + ->setMethods(['getProductLinks']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $firstOption->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$productLink]); + $firstOption->expects($this->once()) + ->method('setOptionId') + ->with($this->identicalTo(null)); + $secondOption = $this->getMockBuilder(BundleOptionInterface::class) + ->setMethods(['getProductLinks']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $secondOption->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$productLink]); + $secondOption->expects($this->once()) + ->method('setOptionId') + ->with($this->identicalTo(null)); $bundleOptions = [ - $this->getMockBuilder(BundleOptionInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(), - $this->getMockBuilder(BundleOptionInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass() + $firstOption, + $secondOption ]; $extensionAttributesProduct->expects($this->once()) ->method('getBundleProductOptions') ->willReturn($bundleOptions); + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $duplicate */ $duplicate = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php index f431012dc3fa5..92326bb1521b4 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePrice.php @@ -39,9 +39,9 @@ public function __construct( $this->locator = $locator; $this->arrayManager = $arrayManager; } - + /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -64,7 +64,7 @@ public function modifyMeta(array $meta) $this->arrayManager->findPath( ProductAttributeInterface::CODE_PRICE, $meta, - null, + self::DEFAULT_GENERAL_PANEL . '/children', 'children' ) . static::META_CONFIG_PATH, $meta, @@ -94,7 +94,7 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { diff --git a/app/code/Magento/Bundle/etc/db_schema.xml b/app/code/Magento/Bundle/etc/db_schema.xml index 97e86e5c17359..dba9732439065 100644 --- a/app/code/Magento/Bundle/etc/db_schema.xml +++ b/app/code/Magento/Bundle/etc/db_schema.xml @@ -10,9 +10,9 @@ <table name="catalog_product_bundle_option" resource="default" engine="innodb" comment="Catalog Product Bundle Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="smallint" name="required" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Required"/> <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="false" identity="false" @@ -31,14 +31,14 @@ <table name="catalog_product_bundle_option_value" resource="default" engine="innodb" comment="Catalog Product Bundle Option Value"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> @@ -54,13 +54,13 @@ <table name="catalog_product_bundle_selection" resource="default" engine="innodb" comment="Catalog Product Bundle Selection"> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Selection Id"/> + comment="Selection ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Position"/> <column xsi:type="smallint" name="is_default" padding="5" unsigned="true" nullable="false" identity="false" @@ -92,15 +92,15 @@ <table name="catalog_product_bundle_selection_price" resource="default" engine="innodb" comment="Catalog Product Bundle Selection Price"> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Selection Id"/> + comment="Selection ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="selection_price_type" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Selection Price Type"/> <column xsi:type="decimal" name="selection_price_value" scale="6" precision="20" unsigned="false" nullable="false" default="0" comment="Selection Price Value"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <constraint xsi:type="primary" referenceId="PK_CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE"> <column name="selection_id"/> <column name="parent_product_id"/> @@ -122,7 +122,7 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Customer Group ID"/> <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="false" @@ -159,7 +159,7 @@ <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Stock ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="smallint" name="stock_status" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Stock Status"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -203,7 +203,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_bundle_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_bundle_tmp" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -246,9 +246,9 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Selection Id"/> + default="0" comment="Selection ID"/> <column xsi:type="smallint" name="group_type" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Group Type"/> <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" @@ -265,7 +265,7 @@ <column name="selection_id"/> </constraint> </table> - <table name="catalog_product_index_price_bundle_sel_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_bundle_sel_tmp" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Sel Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -274,9 +274,9 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Selection Id"/> + default="0" comment="Selection ID"/> <column xsi:type="smallint" name="group_type" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Group Type"/> <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" @@ -302,7 +302,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" comment="Min Price"/> <column xsi:type="decimal" name="alt_price" scale="6" precision="20" unsigned="false" nullable="true" @@ -320,7 +320,7 @@ <column name="option_id"/> </constraint> </table> - <table name="catalog_product_index_price_bundle_opt_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_bundle_opt_tmp" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Opt Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -329,7 +329,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" comment="Min Price"/> <column xsi:type="decimal" name="alt_price" scale="6" precision="20" unsigned="false" nullable="true" diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index d0e956efee694..6c1a5ab2e7257 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -221,4 +221,17 @@ </argument> </arguments> </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="price_type" xsi:type="string">catalog_product</item> + <item name="price_view" xsi:type="string">catalog_product</item> + <item name="shipment_type" xsi:type="string">catalog_product</item> + <item name="sku_type" xsi:type="string">catalog_product</item> + <item name="weight_type" xsi:type="string">catalog_product</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml index 08e89699b1f71..236f15d6b376c 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml @@ -10,7 +10,7 @@ <?php $_selections = $_option->getSelections(); ?> <?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' required _required'; } ?>"> +<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' _required'; } ?>"> <label class="label admin__field-label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml index f4c4e3e51ae09..28b94b21b7889 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml @@ -9,7 +9,7 @@ <?php $_option = $block->getOption(); ?> <?php $_selections = $_option->getSelections(); ?> <?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -<div class="field admin__field <?php if ($_option->getRequired()) { echo ' required'; } ?><?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> +<div class="field admin__field <?php if ($_option->getRequired()) { echo ' _required'; } ?><?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> <div class="control admin__field-control"> <?php if (count($_selections) == 1 && $_option->getRequired()) : ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml index 0c3835fb32af8..185a9159c8ab3 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml @@ -12,7 +12,7 @@ <?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> -<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' required'; } ?>"> +<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' _required'; } ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> <div class="control admin__field-control"> <div class="nested<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> @@ -39,7 +39,7 @@ <?php foreach ($_selections as $_selection) : ?> <div class="field choice admin__field admin__field-option"> <input type="radio" - class="radio admin__control-radio <?= $_option->getRequired() ? ' validate-one-required-by-name' : '' ?> change-container-classname" + class="radio admin__control-radio <?= $_option->getRequired() ? ' required-entry' : '' ?> change-container-classname" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> @@ -66,7 +66,7 @@ </label> <div class="control admin__field-control"><input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" - class="input-text admin__control-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" + class="input-text admin__control-text qty validate-greater-than-zero<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" type="text" name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_defaultQty) ?>" /> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml index fbb7f7fbb7b38..047e25a65af2b 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml @@ -12,7 +12,7 @@ <?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> -<div class="field admin__field option<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?><?php if ($_option->getRequired()) { echo ' required _required'; } ?>"> +<div class="field admin__field option<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?><?php if ($_option->getRequired()) { echo ' _required'; } ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> <div class="control admin__field-control"> <?php if ($block->showSingle()) : ?> @@ -49,7 +49,7 @@ <div class="control admin__field-control"> <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" - class="input-text admin__control-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" + class="input-text admin__control-text qty validate-greater-than-zero<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" type="text" name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_defaultQty) ?>" /> diff --git a/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php new file mode 100644 index 0000000000000..5cdfdc88e7dc1 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Cart; + +use Magento\Bundle\Helper\Catalog\Product\Configuration; +use Magento\Catalog\Model\Product; +use Magento\Quote\Model\Quote\Item; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * Data provider for bundled product options + */ +class BundleOptionDataProvider +{ + /** + * @var Data + */ + private $pricingHelper; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var Configuration + */ + private $configuration; + + /** + * @param Data $pricingHelper + * @param SerializerInterface $serializer + * @param Configuration $configuration + */ + public function __construct( + Data $pricingHelper, + SerializerInterface $serializer, + Configuration $configuration + ) { + $this->pricingHelper = $pricingHelper; + $this->serializer = $serializer; + $this->configuration = $configuration; + } + + /** + * Extract data for a bundled cart item + * + * @param Item $item + * @return array + */ + public function getData(Item $item): array + { + $options = []; + $product = $item->getProduct(); + + /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ + $typeInstance = $product->getTypeInstance(); + + $optionsQuoteItemOption = $item->getOptionByCode('bundle_option_ids'); + $bundleOptionsIds = $optionsQuoteItemOption + ? $this->serializer->unserialize($optionsQuoteItemOption->getValue()) + : []; + + if ($bundleOptionsIds) { + /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */ + $optionsCollection = $typeInstance->getOptionsByIds($bundleOptionsIds, $product); + + $selectionsQuoteItemOption = $item->getOptionByCode('bundle_selection_ids'); + + $bundleSelectionIds = $this->serializer->unserialize($selectionsQuoteItemOption->getValue()); + + if (!empty($bundleSelectionIds)) { + $selectionsCollection = $typeInstance->getSelectionsByIds($bundleSelectionIds, $product); + $bundleOptions = $optionsCollection->appendSelections($selectionsCollection, true); + + $options = $this->buildBundleOptions($bundleOptions, $item); + } + } + + return $options; + } + + /** + * Build bundle product options based on current selection + * + * @param \Magento\Bundle\Model\Option[] $bundleOptions + * @param Item $item + * @return array + */ + private function buildBundleOptions(array $bundleOptions, Item $item): array + { + $options = []; + foreach ($bundleOptions as $bundleOption) { + if (!$bundleOption->getSelections()) { + continue; + } + + $options[] = [ + 'id' => $bundleOption->getId(), + 'label' => $bundleOption->getTitle(), + 'type' => $bundleOption->getType(), + 'values' => $this->buildBundleOptionValues($bundleOption->getSelections(), $item), + ]; + } + + return $options; + } + + /** + * Build bundle product option values based on current selection + * + * @param Product[] $selections + * @param Item $item + * @return array + */ + private function buildBundleOptionValues(array $selections, Item $item): array + { + $values = []; + + $product = $item->getProduct(); + foreach ($selections as $selection) { + $qty = (float) $this->configuration->getSelectionQty($product, $selection->getSelectionId()); + if (!$qty) { + continue; + } + + $selectionPrice = $this->configuration->getSelectionFinalPrice($item, $selection); + + $values[] = [ + 'id' => $selection->getSelectionId(), + 'label' => $selection->getName(), + 'quantity' => $qty, + 'price' => $this->pricingHelper->currency($selectionPrice, false, false), + ]; + } + + return $values; + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Cart/BuyRequest/BundleDataProvider.php b/app/code/Magento/BundleGraphQl/Model/Cart/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..37a9309092166 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Cart/BuyRequest/BundleDataProvider.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Cart\BuyRequest; + +use Magento\Framework\Stdlib\ArrayManager; +use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @param ArrayManager $arrayManager + */ + public function __construct( + ArrayManager $arrayManager + ) { + $this->arrayManager = $arrayManager; + } + + /** + * @inheritdoc + */ + public function execute(array $cartItemData): array + { + $bundleOptions = []; + $bundleInputs = $this->arrayManager->get('bundle_options', $cartItemData) ?? []; + foreach ($bundleInputs as $bundleInput) { + $bundleOptions['bundle_option'][$bundleInput['id']] = $bundleInput['value']; + $bundleOptions['bundle_option_qty'][$bundleInput['id']] = $bundleInput['quantity']; + } + + return $bundleOptions; + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleOption.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleOption.php new file mode 100644 index 0000000000000..6b64310fcb1e3 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleOption.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver; + +use Magento\BundleGraphQl\Model\Cart\BundleOptionDataProvider; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver for bundle product options + */ +class BundleOption implements ResolverInterface +{ + /** + * @var BundleOptionDataProvider + */ + private $dataProvider; + + /** + * @param BundleOptionDataProvider $bundleOptionDataProvider + */ + public function __construct( + BundleOptionDataProvider $bundleOptionDataProvider + ) { + $this->dataProvider = $bundleOptionDataProvider; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + return $this->dataProvider->getData($value['model']); + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..fead6f923d8f0 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver\Product\Price; + +use Magento\Bundle\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Bundle\Model\Product\Price; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; + +/** + * Provides pricing information for Bundle products + */ +class Provider implements ProviderInterface +{ + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getMinimalPrice(); + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getMinimalPrice(); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getMaximalPrice(); + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getMaximalPrice(); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + if ($product->getPriceType() == Price::PRICE_TYPE_FIXED) { + return $product->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getAmount(); + } + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } +} diff --git a/app/code/Magento/BundleGraphQl/composer.json b/app/code/Magento/BundleGraphQl/composer.json index db85c2149ec18..74149f500df8e 100644 --- a/app/code/Magento/BundleGraphQl/composer.json +++ b/app/code/Magento/BundleGraphQl/composer.json @@ -7,6 +7,8 @@ "magento/module-catalog": "*", "magento/module-bundle": "*", "magento/module-catalog-graph-ql": "*", + "magento/module-quote": "*", + "magento/module-quote-graph-ql": "*", "magento/module-store": "*", "magento/framework": "*" }, diff --git a/app/code/Magento/BundleGraphQl/etc/di.xml b/app/code/Magento/BundleGraphQl/etc/di.xml index 4f41f3cb8dc80..15acad7c6bf06 100644 --- a/app/code/Magento/BundleGraphQl/etc/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/di.xml @@ -16,4 +16,11 @@ </argument> </arguments> </type> + <type name="Magento\QuoteGraphQl\Model\Resolver\CartItemTypeResolver"> + <arguments> + <argument name="supportedTypes" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleCartItem</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml index 50a2e32b8c9d5..b847a6672e046 100644 --- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml @@ -13,6 +13,13 @@ </argument> </arguments> </type> + <type name="Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\BundleGraphQl\Model\Cart\BuyRequest\BundleDataProvider</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> <arguments> <argument name="map" xsi:type="array"> @@ -40,4 +47,22 @@ </argument> </arguments> </type> + + + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\BundleGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\AttributeProcessor"> + <arguments> + <argument name="fieldToAttributeMap" xsi:type="array"> + <item name="price_range" xsi:type="array"> + <item name="price_type" xsi:type="string">price_type</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/BundleGraphQl/etc/module.xml b/app/code/Magento/BundleGraphQl/etc/module.xml index 352a46d7c171e..8d6725054867e 100644 --- a/app/code/Magento/BundleGraphQl/etc/module.xml +++ b/app/code/Magento/BundleGraphQl/etc/module.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Magento_BundleGraphQl" > <sequence> + <module name="Magento_QuoteGraphQl"/> <module name="Magento_Catalog"/> <module name="Magento_BundleProduct"/> <module name="Magento_Store"/> diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index 74e21d3feaba2..0eff0e086180e 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -1,6 +1,50 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type Mutation { + addBundleProductsToCart(input: AddBundleProductsToCartInput): AddBundleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") +} + +input AddBundleProductsToCartInput { + cart_id: String! + cart_items: [BundleProductCartItemInput!]! +} + +input BundleProductCartItemInput { + data: CartItemInput! + bundle_options:[BundleOptionInput!]! + customizable_options:[CustomizableOptionInput!] +} + +input BundleOptionInput { + id: Int! + quantity: Float! + value: [String!]! +} + +type AddBundleProductsToCartOutput { + cart: Cart! +} + +type BundleCartItem implements CartItemInterface { + customizable_options: [SelectedCustomizableOption]! @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") + bundle_options: [SelectedBundleOption!]! @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleOption") +} + +type SelectedBundleOption { + id: Int! + label: String! + type: String! + values: [SelectedBundleOptionValue!]! +} + +type SelectedBundleOptionValue { + id: Int! + label: String! + quantity: Float! + price: Float! +} + type BundleItem @doc(description: "BundleItem defines an individual item in a bundle product.") { option_id: Int @doc(description: "An ID assigned to each type of item in a bundle product.") title: String @doc(description: "The display name of the item.") diff --git a/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php b/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php index 91737c1a3d779..8c1da0e1ef104 100644 --- a/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckContactUsFormObserver.php @@ -9,6 +9,9 @@ use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\App\ObjectManager; +/** + * Class CheckContactUsFormObserver + */ class CheckContactUsFormObserver implements ObserverInterface { /** @@ -76,7 +79,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getControllerAction(); if (!$captcha->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { - $this->messageManager->addError(__('Incorrect CAPTCHA.')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA.')); $this->getDataPersistor()->set($formId, $controller->getRequest()->getPostValue()); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->redirect->redirect($controller->getResponse(), 'contact/index/index'); diff --git a/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php b/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php index 0736c7514a568..623d11903926e 100644 --- a/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class CheckForgotpasswordObserver + */ class CheckForgotpasswordObserver implements ObserverInterface { /** @@ -69,7 +72,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getControllerAction(); if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->redirect->redirect($controller->getResponse(), '*/*/forgotpassword'); } diff --git a/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php b/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php index 6d2ed4d1050ca..ef66116432f55 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php @@ -7,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class CheckUserCreateObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckUserCreateObserver implements ObserverInterface { /** @@ -86,7 +91,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Framework\App\Action\Action $controller */ $controller = $observer->getControllerAction(); if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->_session->setCustomerFormData($controller->getRequest()->getPostValue()); $url = $this->_urlManager->getUrl('*/*/create', ['_nosecret' => true]); diff --git a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php index 9d3cd8d367093..872bbec4ffa56 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php @@ -11,13 +11,12 @@ use Magento\Framework\App\Config\ScopeConfigInterface; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Class CheckUserEditObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class CheckUserEditObserver implements ObserverInterface { - /** - * Form ID - */ const FORM_ID = 'user_edit'; /** @@ -96,7 +95,8 @@ public function __construct( * Check Captcha On Forgot Password Page * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return $this|void + * @throws \Magento\Framework\Exception\SessionException */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -119,9 +119,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) 'The account is locked. Please wait and try again or contact %1.', $this->scopeConfig->getValue('contact/email/recipient_email') ); - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->redirect->redirect($controller->getResponse(), '*/*/edit'); } diff --git a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php index 2de93dcf6b59b..e11e48a527169 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php @@ -7,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class CheckUserForgotPasswordBackendObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckUserForgotPasswordBackendObserver implements ObserverInterface { /** @@ -76,7 +81,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ) { $this->_session->setEmail((string)$controller->getRequest()->getPost('email')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $controller->getResponse()->setRedirect( $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true]) ); diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php index dd4974c5d842c..27507423e77eb 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php @@ -6,10 +6,10 @@ namespace Magento\Captcha\Observer; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\AuthenticationInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Api\CustomerRepositoryInterface; /** * Check captcha on user login page observer. @@ -64,6 +64,8 @@ class CheckUserLoginObserver implements ObserverInterface protected $authentication; /** + * CheckUserLoginObserver constructor. + * * @param \Magento\Captcha\Helper\Data $helper * @param \Magento\Framework\App\ActionFlag $actionFlag * @param \Magento\Framework\Message\ManagerInterface $messageManager @@ -125,8 +127,7 @@ private function getAuthentication() * Check captcha on user login page * * @param \Magento\Framework\Event\Observer $observer - * @throws NoSuchEntityException - * @return $this + * @return $this|void */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -143,10 +144,11 @@ public function execute(\Magento\Framework\Event\Observer $observer) try { $customer = $this->getCustomerRepository()->get($login); $this->getAuthentication()->processAuthenticationFailure($customer->getId()); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (NoSuchEntityException $e) { //do nothing as customer existence is validated later in authenticate method } - $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); $this->_session->setUsername($login); $beforeUrl = $this->_session->getBeforeAuthUrl(); diff --git a/app/code/Magento/Captcha/Test/Unit/CustomerData/CaptchaTest.php b/app/code/Magento/Captcha/Test/Unit/CustomerData/CaptchaTest.php new file mode 100644 index 0000000000000..a791039fe27f9 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/CustomerData/CaptchaTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\CustomerData; + +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Captcha\CustomerData\Captcha; +use Magento\Captcha\Model\DefaultModel; +use Magento\Customer\Api\Data\CustomerInterface as CustomerData; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\TestCase; + +class CaptchaTest extends TestCase +{ + /** + * @var CaptchaHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * @var CustomerSession|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerSessionMock; + + /** + * @var Captcha + */ + private $model; + + /** + * @var array + */ + private $formIds; + + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * Create mocks and model + */ + protected function setUp() + { + $this->helperMock = $this->createMock(CaptchaHelper::class); + $this->customerSessionMock = $this->createMock(CustomerSession::class); + $this->formIds = [ + 'user_login' + ]; + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + Captcha::class, + [ + 'helper' => $this->helperMock, + 'formIds' => $this->formIds, + 'customerSession' => $this->customerSessionMock + ] + ); + } + + /** + * Test getSectionData() when user is login and require captcha + */ + public function testGetSectionDataWhenLoginAndRequireCaptcha() + { + $emailLogin = 'test@localhost.com'; + + $userLoginModel = $this->createMock(DefaultModel::class); + $userLoginModel->expects($this->any())->method('isRequired')->with($emailLogin) + ->willReturn(true); + $this->helperMock->expects($this->any())->method('getCaptcha')->with('user_login')->willReturn($userLoginModel); + + $this->customerSessionMock->expects($this->any())->method('isLoggedIn') + ->willReturn(true); + + $customerDataMock = $this->createMock(CustomerData::class); + $customerDataMock->expects($this->any())->method('getEmail')->willReturn($emailLogin); + $this->customerSessionMock->expects($this->any())->method('getCustomerData') + ->willReturn($customerDataMock); + + /* Assert to test */ + $this->assertEquals( + [ + "user_login" => [ + "isRequired" => true, + "timestamp" => time() + ] + ], + $this->model->getSectionData() + ); + } +} diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Config/FontTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Config/FontTest.php new file mode 100644 index 0000000000000..42ab3146f1321 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Model/Config/FontTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Model\Config; + +use PHPUnit\Framework\TestCase; +use Magento\Captcha\Helper\Data as HelperData; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Captcha\Model\Config\Font; + +class FontTest extends TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Font + */ + private $model; + + /** + * @var HelperData|\PHPUnit_Framework_MockObject_MockObject + */ + private $helperDataMock; + + /** + * Setup Environment For Testing + */ + protected function setUp() + { + $this->helperDataMock = $this->createMock(HelperData::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + Font::class, + [ + 'captchaData' => $this->helperDataMock + ] + ); + } + + /** + * Test toOptionArray() with data provider below + * + * @param array $fonts + * @param array $expectedResult + * @dataProvider toOptionArrayDataProvider + */ + public function testToOptionArray($fonts, $expectedResult) + { + $this->helperDataMock->expects($this->any())->method('getFonts') + ->willReturn($fonts); + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } + + /** + * Data Provider for testing toOptionArray() + * + * @return array + */ + public function toOptionArrayDataProvider() + { + return [ + 'Empty get font' => [ + [], + [] + ], + 'Get font result' => [ + [ + 'arial' => [ + 'label' => 'Arial', + 'path' => '/www/magento/fonts/arial.ttf' + ], + 'verdana' => [ + 'label' => 'Verdana', + 'path' => '/www/magento/fonts/verdana.ttf' + ] + ], + [ + [ + 'label' => 'Arial', + 'value' => 'arial' + ], + [ + 'label' => 'Verdana', + 'value' => 'verdana' + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/BackendTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/BackendTest.php new file mode 100644 index 0000000000000..054cc71af61bc --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/BackendTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Model\Config\Form; + +use Magento\Captcha\Model\Config\Form\Backend; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class BackendTest extends TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Backend + */ + private $model; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * Setup Environment For Testing + */ + protected function setUp() + { + $this->configMock = $this->createMock(ScopeConfigInterface::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + Backend::class, + [ + 'config' => $this->configMock + ] + ); + } + + /** + * Test toOptionArray() with data provider below + * + * @param string|array $config + * @param array $expectedResult + * @dataProvider toOptionArrayDataProvider + */ + public function testToOptionArray($config, $expectedResult) + { + $this->configMock->expects($this->any())->method('getValue') + ->with('captcha/backend/areas', 'default') + ->willReturn($config); + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } + + /** + * Data Provider for testing toOptionArray() + * + * @return array + */ + public function toOptionArrayDataProvider() + { + return [ + 'Empty captcha backend areas' => [ + '', + [] + ], + 'With two captcha backend area' => [ + [ + 'backend_login' => [ + 'label' => 'Admin Login' + ], + 'backend_forgotpassword' => [ + 'label' => 'Admin Forgot Password' + ] + ], + [ + [ + 'label' => 'Admin Login', + 'value' => 'backend_login' + ], + [ + 'label' => 'Admin Forgot Password', + 'value' => 'backend_forgotpassword' + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/FrontendTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/FrontendTest.php new file mode 100644 index 0000000000000..d3f40f5872a7d --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/FrontendTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Model\Config\Form; + +use Magento\Captcha\Model\Config\Form\Frontend; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class FrontendTest extends TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Frontend + */ + private $model; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * Setup Environment For Testing + */ + protected function setUp() + { + $this->configMock = $this->createMock(ScopeConfigInterface::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + Frontend::class, + [ + 'config' => $this->configMock + ] + ); + } + + /** + * Test toOptionArray() with data provider below + * + * @param string|array $config + * @param array $expectedResult + * @dataProvider toOptionArrayDataProvider + */ + public function testToOptionArray($config, $expectedResult) + { + $this->configMock->expects($this->any())->method('getValue') + ->with('captcha/frontend/areas', 'default') + ->willReturn($config); + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } + + /** + * Data Provider for testing toOptionArray() + * + * @return array + */ + public function toOptionArrayDataProvider() + { + return [ + 'Empty captcha frontend areas' => [ + '', + [] + ], + 'With two captcha frontend area' => [ + [ + 'product_sendtofriend_form' => [ + 'label' => 'Send To Friend Form' + ], + 'sales_rule_coupon_request' => [ + 'label' => 'Applying coupon code' + ] + ], + [ + [ + 'label' => 'Send To Friend Form', + 'value' => 'product_sendtofriend_form' + ], + [ + 'label' => 'Applying coupon code', + 'value' => 'sales_rule_coupon_request' + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php index 08f76aa74ac6d..83bfb2910f9f8 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php @@ -69,7 +69,10 @@ protected function setUp() $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); $this->captchaStringResolverMock = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); - $this->sessionMock = $this->createPartialMock(\Magento\Framework\Session\SessionManager::class, ['addError']); + $this->sessionMock = $this->createPartialMock( + \Magento\Framework\Session\SessionManager::class, + ['addErrorMessage'] + ); $this->dataPersistorMock = $this->getMockBuilder(\Magento\Framework\App\Request\DataPersistorInterface::class) ->getMockForAbstractClass(); @@ -116,7 +119,7 @@ public function testCheckContactUsFormWhenCaptchaIsRequiredAndValid() $this->helperMock->expects($this->any()) ->method('getCaptcha') ->with($formId)->willReturn($this->captchaMock); - $this->sessionMock->expects($this->never())->method('addError'); + $this->sessionMock->expects($this->never())->method('addErrorMessage'); $this->checkContactUsFormObserver->execute( new \Magento\Framework\Event\Observer(['controller_action' => $controller]) @@ -163,7 +166,7 @@ public function testCheckContactUsFormRedirectsCustomerWithWarningMessageWhenCap ->method('getCaptcha') ->with($formId) ->willReturn($this->captchaMock); - $this->messageManagerMock->expects($this->once())->method('addError')->with($warningMessage); + $this->messageManagerMock->expects($this->once())->method('addErrorMessage')->with($warningMessage); $this->actionFlagMock->expects($this->once()) ->method('set') ->with('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php index b05a3b2e34af0..93b58191cc334 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php @@ -138,7 +138,7 @@ public function testCheckForgotpasswordRedirects() )->will( $this->returnValue($this->_captcha) ); - $this->_messageManager->expects($this->once())->method('addError')->with($warningMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage); $this->_actionFlag->expects( $this->once() )->method( diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php index 8dc67437f4879..a57faabda99eb 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php @@ -151,7 +151,7 @@ public function testCheckUserCreateRedirectsError() )->will( $this->returnValue($this->_captcha) ); - $this->_messageManager->expects($this->once())->method('addError')->with($warningMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage); $this->_actionFlag->expects( $this->once() )->method( diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php index 26fd8fd928c56..0f08e5c569dfc 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php @@ -146,7 +146,7 @@ public function testExecute() $message = __('The account is locked. Please wait and try again or contact %1.', $email); $this->messageManagerMock->expects($this->exactly(2)) - ->method('addError') + ->method('addErrorMessage') ->withConsecutive([$message], [__('Incorrect CAPTCHA')]); $this->actionFlagMock->expects($this->once()) diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php index 19dc096b9ef66..0499ec3255c51 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php @@ -145,7 +145,7 @@ public function testExecute() ->with($customerId); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('Incorrect CAPTCHA')); $this->actionFlagMock->expects($this->once()) diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/Page/AdminThreeDSecurePage.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/Page/AdminThreeDSecurePage.xml new file mode 100644 index 0000000000000..dae6869dbfe79 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Mftf/Page/AdminThreeDSecurePage.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="AdminThreeDSecurePage" url="admin/system_config/edit/section/three_d_secure/" area="admin" module="Magento_CardinalCommerce"> + <section name="AdminCardinalCommerceSection"/> + </page> +</pages> diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/Section/AdminCardinalCommerceSection.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/Section/AdminCardinalCommerceSection.xml new file mode 100644 index 0000000000000..1016fbaefb0ab --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Mftf/Section/AdminCardinalCommerceSection.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="AdminCardinalCommerceSection"> + <element name="head" type="button" selector="#three_d_secure_cardinal_config-link"/> + <element name="enabled" type="input" selector="#three_d_secure_cardinal_config_enabled_authorize"/> + <element name="environment" type="input" selector="#three_d_secure_cardinal_config_environment"/> + </section> +</sections> diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/Test/AdminCardinalCommerceSettingsHiddenTest.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/Test/AdminCardinalCommerceSettingsHiddenTest.xml new file mode 100644 index 0000000000000..a41b96f0db6e4 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Mftf/Test/AdminCardinalCommerceSettingsHiddenTest.xml @@ -0,0 +1,33 @@ +<?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="AdminCardinalCommerceSettingsHiddenTest"> + <annotations> + <features value="CardinalCommerce"/> + <title value="CardinalCommerce settings hidden" /> + <description value="CardinalCommerce config shouldn't be visible if the 3D secure is disabled for Authorize.Net."/> + <severity value="MINOR"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set three_d_secure/cardinal/enabled_authorizenet 1" stepKey="enableCardinalCommerce"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="config:set three_d_secure/cardinal/enabled_authorizenet 0" stepKey="disableCardinalCommerce"/> + </after> + + <amOnPage url="{{AdminThreeDSecurePage.url}}" stepKey="openCurrencyOptionsPage" /> + <conditionalClick dependentSelector="{{AdminCardinalCommerceSection.enabled}}" visible="false" selector="{{AdminCardinalCommerceSection.head}}" stepKey="openCollapsibleBlock"/> + <see selector="{{AdminCardinalCommerceSection.environment}}" userInput="Production" stepKey="seeEnvironmentProduction"/> + <selectOption selector="{{AdminCardinalCommerceSection.enabled}}" userInput="0" stepKey="disableCardinalCommerceOption"/> + <dontSeeElement selector="{{AdminCardinalCommerceSection.environment}}" stepKey="dontSeeEnvironmentProduction"/> + </test> +</tests> diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtParserTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtParserTest.php new file mode 100644 index 0000000000000..7c17c4e2e87d5 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtParserTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Test\Unit\Model\Response; + +use Magento\CardinalCommerce\Model\Response\JwtParser; +use Magento\CardinalCommerce\Model\Config; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\CardinalCommerce\Model\Response\JwtPayloadValidatorInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Class \Magento\CardinalCommerce\Test\Unit\Model\Response\JwtParserTest + */ +class JwtParserTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var JwtParser + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | Config + */ + private $configMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | JwtManagement + */ + private $jwtManagementMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | JwtPayloadValidatorInterface + */ + private $jwtPayloadValidatorMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->configMock = $this->getMockBuilder(Config::class) + ->setMethods(['getApiKey', 'isDebugModeEnabled']) + ->disableOriginalConstructor() + ->getMock(); + + $this->jwtManagementMock = $this->getMockBuilder(JwtManagement::class) + ->setMethods(['decode']) + ->disableOriginalConstructor() + ->getMock(); + + $this->jwtPayloadValidatorMock = $this->getMockBuilder(JwtPayloadValidatorInterface::class) + ->setMethods(['validate']) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = $this->objectManager->getObject( + JwtParser::class, + [ + 'jwtManagement' => $this->jwtManagementMock, + 'config' => $this->configMock, + 'tokenValidator' => $this->jwtPayloadValidatorMock + ] + ); + + $this->configMock->expects($this->any()) + ->method('getApiKey') + ->willReturn('API Key'); + + $this->configMock->expects($this->any()) + ->method('isDebugModeEnabled') + ->willReturn(false); + + $this->jwtManagementMock->expects($this->any()) + ->method('decode') + ->with('string_to_test', 'API Key') + ->willReturn(['mockResult' => 'jwtPayload']); + } + + /** + * Tests Jwt Parser execute with the result and no exception. + */ + public function testExecuteWithNoException() + { + /* Validate Success */ + $this->jwtPayloadValidatorMock->expects($this->any()) + ->method('validate') + ->with(['mockResult' => 'jwtPayload']) + ->willReturn(true); + + /* Assert the result of function */ + $jwtPayload = $this->model->execute('string_to_test'); + $this->assertEquals( + ['mockResult' => 'jwtPayload'], + $jwtPayload + ); + } + + /** + * Tests Jwt Parser execute with exception and no result. + */ + public function testExecuteWithException() + { + /* Validate Fail */ + $this->jwtPayloadValidatorMock->expects($this->any()) + ->method('validate') + ->with(['mockResult' => 'jwtPayload']) + ->willReturn(false); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage( + 'Authentication Failed. Your card issuer cannot authenticate this card. ' . + 'Please select another card or form of payment to complete your purchase.' + ); + + /* Execute function */ + $this->model->execute('string_to_test'); + } +} diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml index 532fcdd0f598f..046475baba676 100644 --- a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml +++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml @@ -19,26 +19,41 @@ <label>Environment</label> <source_model>Magento\CardinalCommerce\Model\Adminhtml\Source\Environment</source_model> <config_path>three_d_secure/cardinal/environment</config_path> + <depends> + <field id="enabled_authorize">1</field> + </depends> </field> <field id="org_unit_id" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Org Unit Id</label> <config_path>three_d_secure/cardinal/org_unit_id</config_path> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <depends> + <field id="enabled_authorize">1</field> + </depends> </field> <field id="api_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> <label>API Key</label> <config_path>three_d_secure/cardinal/api_key</config_path> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <depends> + <field id="enabled_authorize">1</field> + </depends> </field> <field id="api_identifier" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> <label>API Identifier</label> <config_path>three_d_secure/cardinal/api_identifier</config_path> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <depends> + <field id="enabled_authorize">1</field> + </depends> </field> <field id="debug" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Debug</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>three_d_secure/cardinal/debug</config_path> + <depends> + <field id="enabled_authorize">1</field> + </depends> </field> </group> </group> diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index 9a4a9fa768006..929c181bf820c 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -407,6 +407,7 @@ protected function _getNodeJson($node, $level = 0) public function buildNodeName($node) { $result = $this->escapeHtml($node->getName()); + $result .= ' (ID: ' . $node->getId() . ')'; if ($this->_withProductCount) { $result .= ' (' . $node->getProductCount() . ')'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php index a829c058d89bf..c58ed58370e3a 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php @@ -26,7 +26,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea /** * Catalog data * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager = null; @@ -46,7 +46,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea * @param \Magento\Framework\Escaper $escaper * @param \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig * @param \Magento\Framework\View\LayoutInterface $layout - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Backend\Helper\Data $backendData * @param array $data */ @@ -56,7 +56,7 @@ public function __construct( \Magento\Framework\Escaper $escaper, \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig, \Magento\Framework\View\LayoutInterface $layout, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Backend\Helper\Data $backendData, array $data = [] ) { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php index db42bb66c9bd1..60e17599f6dec 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php @@ -11,15 +11,32 @@ class Back extends Generic { /** + * Get Button Data + * * @return array */ public function getButtonData() { return [ 'label' => __('Back'), - 'on_click' => sprintf("location.href = '%s';", $this->getUrl('*/*/')), + 'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()), 'class' => 'back', 'sort_order' => 10 ]; } + /** + * Get URL for back + * + * @return string + */ + private function getBackUrl() + { + if ($this->context->getRequestParam('customerId')) { + return $this->getUrl( + 'customer/index/edit', + ['id' => $this->context->getRequestParam('customerId')] + ); + } + return $this->getUrl('*/*/'); + } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php index e754ab9700517..386fe1333a7e9 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Price.php @@ -19,7 +19,7 @@ class Price extends Extended /** * Catalog data * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -32,14 +32,14 @@ class Price extends Extended * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper * @param \Magento\ProductAlert\Model\PriceFactory $priceFactory - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, \Magento\ProductAlert\Model\PriceFactory $priceFactory, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_priceFactory = $priceFactory; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php index 2c6647fd57be6..ede478cabe783 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts/Stock.php @@ -19,7 +19,7 @@ class Stock extends Extended /** * Catalog data * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -32,14 +32,14 @@ class Stock extends Extended * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper * @param \Magento\ProductAlert\Model\StockFactory $stockFactory - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, \Magento\ProductAlert\Model\StockFactory $stockFactory, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_stockFactory = $stockFactory; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php index 9278b84362e77..782147e1e8ef6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php @@ -18,7 +18,7 @@ class Inventory extends \Magento\Backend\Block\Widget protected $_template = 'Magento_Catalog::catalog/product/tab/inventory.phtml'; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -53,7 +53,7 @@ class Inventory extends \Magento\Backend\Block\Widget * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\CatalogInventory\Model\Source\Backorders $backorders * @param \Magento\CatalogInventory\Model\Source\Stock $stock - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration @@ -63,7 +63,7 @@ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\CatalogInventory\Model\Source\Backorders $backorders, \Magento\CatalogInventory\Model\Source\Stock $stock, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Framework\Registry $coreRegistry, \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php index 42990116e933f..5ffd3d1dda38d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Group/AbstractGroup.php @@ -41,7 +41,7 @@ abstract class AbstractGroup extends Widget implements RendererInterface /** * Catalog data * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -81,7 +81,7 @@ abstract class AbstractGroup extends Widget implements RendererInterface * @param \Magento\Backend\Block\Template\Context $context * @param GroupRepositoryInterface $groupRepository * @param \Magento\Directory\Helper\Data $directoryHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Framework\Registry $registry * @param GroupManagementInterface $groupManagement * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder @@ -92,7 +92,7 @@ public function __construct( \Magento\Backend\Block\Template\Context $context, GroupRepositoryInterface $groupRepository, \Magento\Directory\Helper\Data $directoryHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Framework\Registry $registry, GroupManagementInterface $groupManagement, \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php index 51c326763b09c..37ad3f4bea20e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php @@ -14,7 +14,7 @@ use Magento\Catalog\Helper\Data; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; use Magento\Framework\Json\EncoderInterface; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\Registry; use Magento\Framework\Translate\InlineInterface; @@ -65,7 +65,7 @@ class Tabs extends WidgetTabs protected $_collectionFactory; /** - * @var ModuleManagerInterface + * @var Manager */ protected $_moduleManager; @@ -78,7 +78,7 @@ class Tabs extends WidgetTabs * @param Context $context * @param EncoderInterface $jsonEncoder * @param Session $authSession - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param CollectionFactory $collectionFactory * @param Catalog $helperCatalog * @param Data $catalogData @@ -91,7 +91,7 @@ public function __construct( Context $context, EncoderInterface $jsonEncoder, Session $authSession, - ModuleManagerInterface $moduleManager, + Manager $moduleManager, CollectionFactory $collectionFactory, Catalog $helperCatalog, Data $catalogData, diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php index 7e43f2fc064ad..01408ade56432 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Grid.php @@ -16,7 +16,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -59,7 +59,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Catalog\Model\Product\Type $type * @param \Magento\Catalog\Model\Product\Attribute\Source\Status $status * @param \Magento\Catalog\Model\Product\Visibility $visibility - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -73,7 +73,7 @@ public function __construct( \Magento\Catalog\Model\Product\Type $type, \Magento\Catalog\Model\Product\Attribute\Source\Status $status, \Magento\Catalog\Model\Product\Visibility $visibility, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_websiteFactory = $websiteFactory; diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php index 088619511545f..24811d61a7715 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php @@ -46,7 +46,7 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements protected $_checkoutCart; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -55,7 +55,7 @@ class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements * @param \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart * @param \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility * @param \Magento\Checkout\Model\Session $checkoutSession - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( @@ -63,7 +63,7 @@ public function __construct( \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart, \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility, \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_checkoutCart = $checkoutCart; @@ -141,6 +141,7 @@ public function getIdentities() { $identities = []; foreach ($this->getItems() as $item) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $identities = array_merge($identities, $item->getIdentities()); } return $identities; diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php index d888f44a6fbfb..fa1beaf6e0ea8 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php @@ -60,7 +60,7 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements protected $_checkoutCart; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -69,7 +69,7 @@ class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements * @param \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart * @param \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility * @param \Magento\Checkout\Model\Session $checkoutSession - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param array $data */ public function __construct( @@ -77,7 +77,7 @@ public function __construct( \Magento\Checkout\Model\ResourceModel\Cart $checkoutCart, \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility, \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, array $data = [] ) { $this->_checkoutCart = $checkoutCart; @@ -264,6 +264,7 @@ public function getIdentities() { $identities = []; foreach ($this->getItems() as $item) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $identities = array_merge($identities, $item->getIdentities()); } return $identities; diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php index e76c5bf201334..38925e9ae3cd7 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Details.php +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -37,11 +37,11 @@ public function getGroupSortedChildNames(string $groupName, string $callback): a $alias = $layout->getElementAlias($childName); $sortOrder = (int)$this->getChildData($alias, 'sort_order') ?? 0; - $childNamesSortOrder[$sortOrder] = $childName; + $childNamesSortOrder[$childName] = $sortOrder; } - ksort($childNamesSortOrder, SORT_NUMERIC); + asort($childNamesSortOrder, SORT_NUMERIC); - return $childNamesSortOrder; + return array_keys($childNamesSortOrder); } } diff --git a/app/code/Magento/Catalog/Block/Widget/Link.php b/app/code/Magento/Catalog/Block/Widget/Link.php index 85e50dbd3dc27..a25af297111d2 100644 --- a/app/code/Magento/Catalog/Block/Widget/Link.php +++ b/app/code/Magento/Catalog/Block/Widget/Link.php @@ -4,17 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Widget to display catalog link - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Widget; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +/** + * Render the URL of given entity + */ class Link extends \Magento\Framework\View\Element\Html\Link implements \Magento\Widget\Block\BlockInterface { /** @@ -63,10 +61,9 @@ public function __construct( /** * Prepare url using passed id path and return it - * or return false if path was not found in url rewrites. * * @throws \RuntimeException - * @return string|false + * @return string|false if path was not found in url rewrites. * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getHref() @@ -93,7 +90,7 @@ public function getHref() if ($rewrite) { $href = $store->getUrl('', ['_direct' => $rewrite->getRequestPath()]); - if (strpos($href, '___store') === false) { + if ($this->addStoreCodeParam($store, $href)) { $href .= (strpos($href, '?') === false ? '?' : '&') . '___store=' . $store->getCode(); } } @@ -102,6 +99,22 @@ public function getHref() return $this->_href; } + /** + * Checks whether store code query param should be appended to the URL + * + * @param \Magento\Store\Model\Store $store + * @param string $url + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function addStoreCodeParam(\Magento\Store\Model\Store $store, string $url): bool + { + return $this->getStoreId() + && !$store->isUseStoreInUrl() + && $store->getId() !== $this->_storeManager->getStore()->getId() + && strpos($url, '___store') === false; + } + /** * Parse id_path * @@ -121,6 +134,7 @@ protected function parseIdPath($idPath) /** * Prepare label using passed text as parameter. + * * If anchor text was not specified get entity name from DB. * * @return string @@ -150,9 +164,8 @@ public function getLabel() /** * Render block HTML - * or return empty string if url can't be prepared * - * @return string + * @return string empty string if url can't be prepared */ protected function _toHtml() { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php index 1e0cb9f197a51..de8c402c7e761 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php @@ -7,10 +7,13 @@ namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\Store; +use Magento\Framework\Controller\ResultFactory; /** * Catalog category controller + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class Category extends \Magento\Backend\App\Action { @@ -26,20 +29,61 @@ abstract class Category extends \Magento\Backend\App\Action */ protected $dateFilter; + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Framework\Registry + */ + private $registry; + + /** + * @var \Magento\Cms\Model\Wysiwyg\Config + */ + private $wysiwigConfig; + + /** + * @var \Magento\Backend\Model\Auth\Session + */ + private $authSession; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Stdlib\DateTime\Filter\Date|null $dateFilter + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Registry $registry + * @param \Magento\Cms\Model\Wysiwyg\Config $wysiwigConfig + * @param \Magento\Backend\Model\Auth\Session $authSession */ public function __construct( \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter = null + \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter = null, + \Magento\Store\Model\StoreManagerInterface $storeManager = null, + \Magento\Framework\Registry $registry = null, + \Magento\Cms\Model\Wysiwyg\Config $wysiwigConfig = null, + \Magento\Backend\Model\Auth\Session $authSession = null ) { $this->dateFilter = $dateFilter; + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get( + \Magento\Store\Model\StoreManagerInterface::class + ); + $this->registry = $registry ?: ObjectManager::getInstance()->get( + \Magento\Framework\Registry::class + ); + $this->wysiwigConfig = $wysiwigConfig ?: ObjectManager::getInstance()->get( + \Magento\Cms\Model\Wysiwyg\Config::class + ); + $this->authSession = $authSession ?: ObjectManager::getInstance()->get( + \Magento\Backend\Model\Auth\Session::class + ); parent::__construct($context); } /** * Initialize requested category and put it into registry. + * * Root category can be returned, if inappropriate store/category is specified * * @param bool $getRootInstead @@ -55,11 +99,7 @@ protected function _initCategory($getRootInstead = false) if ($categoryId) { $category->load($categoryId); if ($storeId) { - $rootId = $this->_objectManager->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore( - $storeId - )->getRootCategoryId(); + $rootId = $this->storeManager->getStore($storeId)->getRootCategoryId(); if (!in_array($rootId, $category->getPathIds())) { // load root category instead wrong one if ($getRootInstead) { @@ -71,10 +111,11 @@ protected function _initCategory($getRootInstead = false) } } - $this->_objectManager->get(\Magento\Framework\Registry::class)->register('category', $category); - $this->_objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); - $this->_objectManager->get(\Magento\Cms\Model\Wysiwyg\Config::class) - ->setStoreId($storeId); + $this->registry->unregister('category'); + $this->registry->unregister('current_category'); + $this->registry->register('category', $category); + $this->registry->register('current_category', $category); + $this->wysiwigConfig->setStoreId($storeId); return $category; } @@ -91,9 +132,8 @@ private function resolveCategoryId() : int } /** - * Resolve store id + * Resolve store Id, tries to take store id from store HTTP parameter * - * Tries to take store id from store HTTP parameter * @see Store * * @return int @@ -121,11 +161,7 @@ protected function ajaxRequestResponse($category, $resultPage) $breadcrumbsPath = $category->getPath(); if (empty($breadcrumbsPath)) { // but if no category, and it is deleted - prepare breadcrumbs from path, saved in session - $breadcrumbsPath = $this->_objectManager->get( - \Magento\Backend\Model\Auth\Session::class - )->getDeletedPath( - true - ); + $breadcrumbsPath = $this->authSession->getDeletedPath(true); if (!empty($breadcrumbsPath)) { $breadcrumbsPath = explode('/', $breadcrumbsPath); // no need to get parent breadcrumbs if deleting category level 1 @@ -138,19 +174,21 @@ protected function ajaxRequestResponse($category, $resultPage) } } - $eventResponse = new \Magento\Framework\DataObject([ - 'content' => $resultPage->getLayout()->getUiComponent('category_form')->getFormHtml() - . $resultPage->getLayout()->getBlock('category.tree') - ->getBreadcrumbsJavascript($breadcrumbsPath, 'editingCategoryBreadcrumbs'), - 'messages' => $resultPage->getLayout()->getMessagesBlock()->getGroupedHtml(), - 'toolbar' => $resultPage->getLayout()->getBlock('page.actions.toolbar')->toHtml() - ]); + $eventResponse = new \Magento\Framework\DataObject( + [ + 'content' => $resultPage->getLayout()->getUiComponent('category_form')->getFormHtml() + . $resultPage->getLayout()->getBlock('category.tree') + ->getBreadcrumbsJavascript($breadcrumbsPath, 'editingCategoryBreadcrumbs'), + 'messages' => $resultPage->getLayout()->getMessagesBlock()->getGroupedHtml(), + 'toolbar' => $resultPage->getLayout()->getBlock('page.actions.toolbar')->toHtml() + ] + ); $this->_eventManager->dispatch( 'category_prepare_ajax_response', ['response' => $eventResponse, 'controller' => $this] ); /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->_objectManager->get(\Magento\Framework\Controller\Result\Json::class); + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); $resultJson->setHeader('Content-type', 'application/json', true); $resultJson->setData($eventResponse->getData()); return $resultJson; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php index 733e270174e4c..8af59dfeaf76a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php @@ -6,31 +6,41 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\Controller\ResultFactory; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; +use Magento\Catalog\Controller\Adminhtml\Category; +use Magento\Backend\Model\View\Result\ForwardFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; /** * Class Add Category * * @package Magento\Catalog\Controller\Adminhtml\Category */ -class Add extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface +class Add extends Category implements HttpGetActionInterface { /** * Forward factory for result * - * @var \Magento\Backend\Model\View\Result\ForwardFactory + * @deprecated Unused Class: ForwardFactory + * @see $this->resultFactory->create() + * @var ForwardFactory + * */ protected $resultForwardFactory; /** * Add category constructor * - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param Context $context + * @param ForwardFactory $resultForwardFactory */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + Context $context, + ForwardFactory $resultForwardFactory ) { parent::__construct($context); $this->resultForwardFactory = $resultForwardFactory; @@ -39,7 +49,7 @@ public function __construct( /** * Add new category form * - * @return \Magento\Backend\Model\View\Result\Forward + * @return ResultInterface */ public function execute() { @@ -47,7 +57,7 @@ public function execute() $category = $this->_initCategory(true); if (!$category || !$parentId || $category->getId()) { - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('catalog/*/', ['_current' => true, 'id' => null]); } @@ -61,9 +71,8 @@ public function execute() $category->addData($categoryData); } - $resultPageFactory = $this->_objectManager->get(\Magento\Framework\View\Result\PageFactory::class); - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ - $resultPage = $resultPageFactory->create(); + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); if ($this->getRequest()->getQuery('isAjax')) { return $this->ajaxRequestResponse($category, $resultPage); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php index 752257f5b9009..9c3aeba6dc914 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php @@ -1,13 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Category; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +/** + * Class CategoriesJson + */ class CategoriesJson extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface { /** @@ -20,19 +23,28 @@ class CategoriesJson extends \Magento\Catalog\Controller\Adminhtml\Category impl */ protected $layoutFactory; + /** + * @var \Magento\Backend\Model\Auth\Session + */ + private $authSession; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Backend\Model\Auth\Session $authSession */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Backend\Model\Auth\Session $authSession = null ) { parent::__construct($context); $this->resultJsonFactory = $resultJsonFactory; $this->layoutFactory = $layoutFactory; + $this->authSession = $authSession ?: ObjectManager::getInstance() + ->get(\Magento\Backend\Model\Auth\Session::class); } /** @@ -43,9 +55,9 @@ public function __construct( public function execute() { if ($this->getRequest()->getParam('expand_all')) { - $this->_objectManager->get(\Magento\Backend\Model\Auth\Session::class)->setIsTreeWasExpanded(true); + $this->authSession->setIsTreeWasExpanded(true); } else { - $this->_objectManager->get(\Magento\Backend\Model\Auth\Session::class)->setIsTreeWasExpanded(false); + $this->authSession->setIsTreeWasExpanded(false); } $categoryId = (int)$this->getRequest()->getPost('id'); if ($categoryId) { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php index 0450ff1607a09..57b5d74e1eaf5 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php @@ -1,13 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Category; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; +/** + * Class Edit + */ class Edit extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface { /** @@ -27,18 +30,23 @@ class Edit extends \Magento\Catalog\Controller\Adminhtml\Category implements Htt /** * Edit constructor. + * * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { parent::__construct($context); $this->resultPageFactory = $resultPageFactory; $this->resultJsonFactory = $resultJsonFactory; + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(\Magento\Store\Model\StoreManagerInterface::class); } /** @@ -51,20 +59,20 @@ public function __construct( public function execute() { $storeId = (int)$this->getRequest()->getParam('store'); - $store = $this->getStoreManager()->getStore($storeId); - $this->getStoreManager()->setCurrentStore($store->getCode()); + $store = $this->storeManager->getStore($storeId); + $this->storeManager->setCurrentStore($store->getCode()); $categoryId = (int)$this->getRequest()->getParam('id'); if (!$categoryId) { if ($storeId) { - $categoryId = (int)$this->getStoreManager()->getStore($storeId)->getRootCategoryId(); + $categoryId = (int)$this->storeManager->getStore($storeId)->getRootCategoryId(); } else { - $defaultStoreView = $this->getStoreManager()->getDefaultStoreView(); + $defaultStoreView = $this->storeManager->getDefaultStoreView(); if ($defaultStoreView) { $categoryId = (int)$defaultStoreView->getRootCategoryId(); } else { - $stores = $this->getStoreManager()->getStores(); + $stores = $this->storeManager->getStores(); if (count($stores)) { $store = reset($stores); $categoryId = (int)$store->getRootCategoryId(); @@ -109,16 +117,4 @@ public function execute() return $resultPage; } - - /** - * @return \Magento\Store\Model\StoreManagerInterface - */ - private function getStoreManager() - { - if (null === $this->storeManager) { - $this->storeManager = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreManagerInterface::class); - } - return $this->storeManager; - } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index 77518fd9bf5cc..9684938d38477 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Catalog\Api\Data\CategoryAttributeInterface; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; /** @@ -56,6 +57,11 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category implements Htt */ private $eavConfig; + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + /** * Constructor * @@ -66,6 +72,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Category implements Htt * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter * @param StoreManagerInterface $storeManager * @param \Magento\Eav\Model\Config $eavConfig + * @param \Psr\Log\LoggerInterface $logger */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -74,15 +81,18 @@ public function __construct( \Magento\Framework\View\LayoutFactory $layoutFactory, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, StoreManagerInterface $storeManager, - \Magento\Eav\Model\Config $eavConfig = null + \Magento\Eav\Model\Config $eavConfig = null, + \Psr\Log\LoggerInterface $logger = null ) { parent::__construct($context, $dateFilter); $this->resultRawFactory = $resultRawFactory; $this->resultJsonFactory = $resultJsonFactory; $this->layoutFactory = $layoutFactory; $this->storeManager = $storeManager; - $this->eavConfig = $eavConfig - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class); + $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() + ->get(\Magento\Eav\Model\Config::class); + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(\Psr\Log\LoggerInterface::class); } /** @@ -173,29 +183,29 @@ public function execute() $products = json_decode($categoryPostData['category_products'], true); $category->setPostedProducts($products); } - $this->_eventManager->dispatch( - 'catalog_category_prepare_save', - ['category' => $category, 'request' => $this->getRequest()] - ); - /** - * Check "Use Default Value" checkboxes values - */ - if (isset($categoryPostData['use_default']) && !empty($categoryPostData['use_default'])) { - foreach ($categoryPostData['use_default'] as $attributeCode => $attributeValue) { - if ($attributeValue) { - $category->setData($attributeCode, null); + try { + $this->_eventManager->dispatch( + 'catalog_category_prepare_save', + ['category' => $category, 'request' => $this->getRequest()] + ); + /** + * Check "Use Default Value" checkboxes values + */ + if (isset($categoryPostData['use_default']) && !empty($categoryPostData['use_default'])) { + foreach ($categoryPostData['use_default'] as $attributeCode => $attributeValue) { + if ($attributeValue) { + $category->setData($attributeCode, null); + } } } - } - /** - * Proceed with $_POST['use_config'] - * set into category model for processing through validation - */ - $category->setData('use_post_data_config', $useConfig); + /** + * Proceed with $_POST['use_config'] + * set into category model for processing through validation + */ + $category->setData('use_post_data_config', $useConfig); - try { $categoryResource = $category->getResource(); if ($category->hasCustomDesignTo()) { $categoryResource->getAttribute('custom_design_from')->setMaxValue($category->getCustomDesignTo()); @@ -210,7 +220,9 @@ public function execute() __('The "%1" attribute is required. Enter and try again.', $attribute) ); } else { - throw new \Exception($error); + $this->messageManager->addErrorMessage(__('Something went wrong while saving the category.')); + $this->logger->critical('Something went wrong while saving the category.'); + $this->_getSession()->setCategoryData($categoryPostData); } } } @@ -219,13 +231,15 @@ public function execute() $category->save(); $this->messageManager->addSuccessMessage(__('You saved the category.')); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addExceptionMessage($e); - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->logger->critical($e); $this->_getSession()->setCategoryData($categoryPostData); - } catch (\Exception $e) { + // phpcs:disable Magento2.Exceptions.ThrowCatch + } catch (\Throwable $e) { $this->messageManager->addErrorMessage(__('Something went wrong while saving the category.')); - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->logger->critical($e); $this->_getSession()->setCategoryData($categoryPostData); } } @@ -332,11 +346,7 @@ protected function getParentCategory($parentId, $storeId) { if (!$parentId) { if ($storeId) { - $parentId = $this->_objectManager->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore( - $storeId - )->getRootCategoryId(); + $parentId = $this->storeManager->getStore($storeId)->getRootCategoryId(); } else { $parentId = \Magento\Catalog\Model\Category::TREE_ROOT_ID; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 342bbc388f872..2d10d7148fb71 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -7,8 +7,10 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Eav\Model\Config; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** @@ -47,6 +49,16 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut */ private $bulkSize; + /** + * @var TimezoneInterface + */ + private $timezone; + + /** + * @var Config + */ + private $eavConfig; + /** * @param Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper @@ -56,6 +68,9 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut * @param \Magento\Framework\Serialize\SerializerInterface $serializer * @param \Magento\Authorization\Model\UserContextInterface $userContext * @param int $bulkSize + * @param TimezoneInterface $timezone + * @param Config $eavConfig + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Action\Context $context, @@ -65,7 +80,9 @@ public function __construct( \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService, \Magento\Framework\Serialize\SerializerInterface $serializer, \Magento\Authorization\Model\UserContextInterface $userContext, - int $bulkSize = 100 + int $bulkSize = 100, + TimezoneInterface $timezone = null, + Config $eavConfig = null ) { parent::__construct($context, $attributeHelper); $this->bulkManagement = $bulkManagement; @@ -74,6 +91,10 @@ public function __construct( $this->serializer = $serializer; $this->userContext = $userContext; $this->bulkSize = $bulkSize; + $this->timezone = $timezone ?: ObjectManager::getInstance() + ->get(TimezoneInterface::class); + $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() + ->get(Config::class); } /** @@ -122,11 +143,10 @@ public function execute() */ private function sanitizeProductAttributes($attributesData) { - $dateFormat = $this->_objectManager->get(TimezoneInterface::class)->getDateFormat(\IntlDateFormatter::SHORT); - $config = $this->_objectManager->get(\Magento\Eav\Model\Config::class); + $dateFormat = $this->timezone->getDateFormat(\IntlDateFormatter::SHORT); foreach ($attributesData as $attributeCode => $value) { - $attribute = $config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); if (!$attribute->getAttributeId()) { unset($attributesData[$attributeCode]); continue; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php index 30a6629dd1c29..4a3de5f6e6eb0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,7 +8,11 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute as AttributeAction; +use Magento\Framework\App\ObjectManager; +/** + * Class Validate + */ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface { /** @@ -22,21 +25,30 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo */ protected $layoutFactory; + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Eav\Model\Config $eavConfig */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Eav\Model\Config $eavConfig = null ) { parent::__construct($context, $attributeHelper); $this->resultJsonFactory = $resultJsonFactory; $this->layoutFactory = $layoutFactory; + $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() + ->get(\Magento\Eav\Model\Config::class); } /** @@ -54,8 +66,7 @@ public function execute() try { if ($attributesData) { foreach ($attributesData as $attributeCode => $value) { - $attribute = $this->_objectManager->get(\Magento\Eav\Model\Config::class) - ->getAttribute('catalog_product', $attributeCode); + $attribute = $this->eavConfig->getAttribute('catalog_product', $attributeCode); if (!$attribute->getAttributeId()) { unset($attributesData[$attributeCode]); continue; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php index 09eacbbf0731c..a05602403e08f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php @@ -17,16 +17,16 @@ use Magento\Eav\Api\Data\AttributeGroupInterfaceFactory; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Api\Data\AttributeSetInterface; +use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\App\ObjectManager; -use Psr\Log\LoggerInterface; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Api\ExtensionAttributesFactory; +use Psr\Log\LoggerInterface; /** * Class AddAttributeToTemplate @@ -86,20 +86,49 @@ class AddAttributeToTemplate extends Product implements HttpPostActionInterface * @param Context $context * @param Builder $productBuilder * @param JsonFactory $resultJsonFactory - * @param AttributeGroupInterfaceFactory|null $attributeGroupFactory + * @param AttributeGroupInterfaceFactory $attributeGroupFactory + * @param AttributeRepositoryInterface $attributeRepository + * @param AttributeSetRepositoryInterface $attributeSetRepository + * @param AttributeGroupRepositoryInterface $attributeGroupRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param AttributeManagementInterface $attributeManagement + * @param LoggerInterface $logger + * @param ExtensionAttributesFactory $extensionAttributesFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( Context $context, Builder $productBuilder, JsonFactory $resultJsonFactory, - AttributeGroupInterfaceFactory $attributeGroupFactory = null + AttributeGroupInterfaceFactory $attributeGroupFactory = null, + AttributeRepositoryInterface $attributeRepository = null, + AttributeSetRepositoryInterface $attributeSetRepository = null, + AttributeGroupRepositoryInterface $attributeGroupRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null, + AttributeManagementInterface $attributeManagement = null, + LoggerInterface $logger = null, + ExtensionAttributesFactory $extensionAttributesFactory = null ) { parent::__construct($context, $productBuilder); $this->resultJsonFactory = $resultJsonFactory; $this->attributeGroupFactory = $attributeGroupFactory ?: ObjectManager::getInstance() ->get(AttributeGroupInterfaceFactory::class); + $this->attributeRepository = $attributeRepository ?: ObjectManager::getInstance() + ->get(AttributeRepositoryInterface::class); + $this->attributeSetRepository = $attributeSetRepository ?: ObjectManager::getInstance() + ->get(AttributeSetRepositoryInterface::class); + $this->attributeGroupRepository = $attributeGroupRepository ?: ObjectManager::getInstance() + ->get(AttributeGroupRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); + $this->attributeManagement = $attributeManagement ?: ObjectManager::getInstance() + ->get(AttributeManagementInterface::class); + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(LoggerInterface::class); + $this->extensionAttributesFactory = $extensionAttributesFactory ?: ObjectManager::getInstance() + ->get(ExtensionAttributesFactory::class); } /** @@ -115,13 +144,13 @@ public function execute() try { /** @var AttributeSetInterface $attributeSet */ - $attributeSet = $this->getAttributeSetRepository()->get($request->getParam('templateId')); + $attributeSet = $this->attributeSetRepository->get($request->getParam('templateId')); $groupCode = $request->getParam('groupCode'); $groupName = $request->getParam('groupName'); $groupSortOrder = $request->getParam('groupSortOrder'); $attributeSearchCriteria = $this->getBasicAttributeSearchCriteriaBuilder()->create(); - $attributeGroupSearchCriteria = $this->getSearchCriteriaBuilder() + $attributeGroupSearchCriteria = $this->searchCriteriaBuilder ->addFilter('attribute_set_id', $attributeSet->getAttributeSetId()) ->addFilter('attribute_group_code', $groupCode) ->setPageSize(1) @@ -129,22 +158,24 @@ public function execute() try { /** @var AttributeGroupInterface[] $attributeGroupItems */ - $attributeGroupItems = $this->getAttributeGroupRepository()->getList($attributeGroupSearchCriteria) + $attributeGroupItems = $this->attributeGroupRepository + ->getList($attributeGroupSearchCriteria) ->getItems(); - if (!$attributeGroupItems) { - throw new NoSuchEntityException; + if ($attributeGroupItems) { + /** @var AttributeGroupInterface $attributeGroup */ + $attributeGroup = reset($attributeGroupItems); + } else { + /** @var AttributeGroupInterface $attributeGroup */ + $attributeGroup = $this->attributeGroupFactory->create(); } - - /** @var AttributeGroupInterface $attributeGroup */ - $attributeGroup = reset($attributeGroupItems); } catch (NoSuchEntityException $e) { /** @var AttributeGroupInterface $attributeGroup */ $attributeGroup = $this->attributeGroupFactory->create(); } $extensionAttributes = $attributeGroup->getExtensionAttributes() - ?: $this->getExtensionAttributesFactory()->create(AttributeGroupInterface::class); + ?: $this->extensionAttributesFactory->create(AttributeGroupInterface::class); $extensionAttributes->setAttributeGroupCode($groupCode); $extensionAttributes->setSortOrder($groupSortOrder); @@ -152,28 +183,31 @@ public function execute() $attributeGroup->setAttributeSetId($attributeSet->getAttributeSetId()); $attributeGroup->setExtensionAttributes($extensionAttributes); - $this->getAttributeGroupRepository()->save($attributeGroup); + $this->attributeGroupRepository->save($attributeGroup); /** @var AttributeInterface[] $attributesItems */ - $attributesItems = $this->getAttributeRepository()->getList( + $attributesItems = $this->attributeRepository->getList( ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeSearchCriteria )->getItems(); - array_walk($attributesItems, function (AttributeInterface $attribute) use ($attributeSet, $attributeGroup) { - $this->getAttributeManagement()->assign( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeSet->getAttributeSetId(), - $attributeGroup->getAttributeGroupId(), - $attribute->getAttributeCode(), - '0' - ); - }); + array_walk( + $attributesItems, + function (AttributeInterface $attribute) use ($attributeSet, $attributeGroup) { + $this->attributeManagement->assign( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSet->getAttributeSetId(), + $attributeGroup->getAttributeGroupId(), + $attribute->getAttributeCode(), + '0' + ); + } + ); } catch (LocalizedException $e) { $response->setError(true); $response->setMessage($e->getMessage()); } catch (\Exception $e) { - $this->getLogger()->critical($e); + $this->logger->critical($e); $response->setError(true); $response->setMessage(__('Unable to add attribute')); } @@ -195,105 +229,10 @@ private function getBasicAttributeSearchCriteriaBuilder() throw new LocalizedException(__('Attributes were missing and must be specified.')); } - return $this->getSearchCriteriaBuilder() - ->addFilter('attribute_id', [$attributeIds['selected']], 'in'); - } - - /** - * Get AttributeRepositoryInterface - * - * @return AttributeRepositoryInterface - */ - private function getAttributeRepository() - { - if (null === $this->attributeRepository) { - $this->attributeRepository = ObjectManager::getInstance() - ->get(AttributeRepositoryInterface::class); - } - return $this->attributeRepository; - } - - /** - * Get AttributeSetRepositoryInterface - * - * @return AttributeSetRepositoryInterface - */ - private function getAttributeSetRepository() - { - if (null === $this->attributeSetRepository) { - $this->attributeSetRepository = ObjectManager::getInstance() - ->get(AttributeSetRepositoryInterface::class); - } - return $this->attributeSetRepository; - } - - /** - * Get AttributeGroupInterface - * - * @return AttributeGroupRepositoryInterface - */ - private function getAttributeGroupRepository() - { - if (null === $this->attributeGroupRepository) { - $this->attributeGroupRepository = ObjectManager::getInstance() - ->get(AttributeGroupRepositoryInterface::class); - } - return $this->attributeGroupRepository; - } - - /** - * Get SearchCriteriaBuilder - * - * @return SearchCriteriaBuilder - */ - private function getSearchCriteriaBuilder() - { - if (null === $this->searchCriteriaBuilder) { - $this->searchCriteriaBuilder = ObjectManager::getInstance() - ->get(SearchCriteriaBuilder::class); - } - return $this->searchCriteriaBuilder; - } - - /** - * Get AttributeManagementInterface - * - * @return AttributeManagementInterface - */ - private function getAttributeManagement() - { - if (null === $this->attributeManagement) { - $this->attributeManagement = ObjectManager::getInstance() - ->get(AttributeManagementInterface::class); - } - return $this->attributeManagement; - } - - /** - * Get LoggerInterface - * - * @return LoggerInterface - */ - private function getLogger() - { - if (null === $this->logger) { - $this->logger = ObjectManager::getInstance() - ->get(LoggerInterface::class); - } - return $this->logger; - } - - /** - * Get ExtensionAttributesFactory. - * - * @return ExtensionAttributesFactory - */ - private function getExtensionAttributesFactory() - { - if (null === $this->extensionAttributesFactory) { - $this->extensionAttributesFactory = ObjectManager::getInstance() - ->get(ExtensionAttributesFactory::class); - } - return $this->extensionAttributesFactory; + return $this->searchCriteriaBuilder->addFilter( + 'attribute_id', + [$attributeIds['selected']], + 'in' + ); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index c74a382724a00..dcb7074c0d036 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -13,6 +12,7 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +use Magento\Framework\Escaper; use Magento\Framework\Serialize\Serializer\FormData; /** @@ -49,6 +49,11 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo */ private $attributeCodeValidator; + /** + * @var Escaper + */ + private $escaper; + /** * Constructor * @@ -61,6 +66,8 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo * @param array $multipleAttributeList * @param FormData|null $formDataSerializer * @param AttributeCodeValidator|null $attributeCodeValidator + * @param Escaper $escaper + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -71,7 +78,8 @@ public function __construct( \Magento\Framework\View\LayoutFactory $layoutFactory, array $multipleAttributeList = [], FormData $formDataSerializer = null, - AttributeCodeValidator $attributeCodeValidator = null + AttributeCodeValidator $attributeCodeValidator = null, + Escaper $escaper = null ) { parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); $this->resultJsonFactory = $resultJsonFactory; @@ -79,9 +87,10 @@ public function __construct( $this->multipleAttributeList = $multipleAttributeList; $this->formDataSerializer = $formDataSerializer ?: ObjectManager::getInstance() ->get(FormData::class); - $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( - AttributeCodeValidator::class - ); + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance() + ->get(AttributeCodeValidator::class); + $this->escaper = $escaper ?: ObjectManager::getInstance() + ->get(Escaper::class); } /** @@ -99,8 +108,10 @@ public function execute() $optionsData = $this->formDataSerializer ->unserialize($this->getRequest()->getParam('serialized_options', '[]')); } catch (\InvalidArgumentException $e) { - $message = __("The attribute couldn't be validated due to an error. Verify your information and try again. " - . "If the error persists, please try again later."); + $message = __( + "The attribute couldn't be validated due to an error. Verify your information and try again. " + . "If the error persists, please try again later." + ); $this->setMessageToResponse($response, [$message]); $response->setError(true); } @@ -138,7 +149,7 @@ public function execute() $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { - $setName = $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($setName); + $setName = $this->escaper->escapeHtml($setName); $this->messageManager->addErrorMessage(__('An attribute set named \'%1\' already exists.', $setName)); $layout = $this->layoutFactory->create(); @@ -241,7 +252,7 @@ private function checkUniqueOption(DataObject $response, array $options = null) private function checkEmptyOption(DataObject $response, array $optionsForCheck = null) { foreach ($optionsForCheck as $optionValues) { - if (isset($optionValues[0]) && $optionValues[0] == '') { + if (isset($optionValues[0]) && trim($optionValues[0]) == '') { $this->setMessageToResponse($response, [__("The value of Admin scope can't be empty.")]); $response->setError(true); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php index 78ad9f423871f..1f959a22b1ba1 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php @@ -115,6 +115,9 @@ public function build(RequestInterface $request): ProductInterface $store = $this->storeFactory->create(); $store->load($storeId); + $this->registry->unregister('product'); + $this->registry->unregister('current_product'); + $this->registry->unregister('current_store'); $this->registry->register('product', $product); $this->registry->register('current_product', $product); $this->registry->register('current_store', $store); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php index 63e52eead064c..90b8ee3164183 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,25 +7,39 @@ use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\ObjectManager; -class Duplicate extends \Magento\Catalog\Controller\Adminhtml\Product +/** + * Class Duplicate + */ +class Duplicate extends \Magento\Catalog\Controller\Adminhtml\Product implements + \Magento\Framework\App\Action\HttpGetActionInterface { /** * @var \Magento\Catalog\Model\Product\Copier */ protected $productCopier; + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + /** * @param Action\Context $context * @param Builder $productBuilder * @param \Magento\Catalog\Model\Product\Copier $productCopier + * @param \Psr\Log\LoggerInterface $logger */ public function __construct( \Magento\Backend\App\Action\Context $context, Product\Builder $productBuilder, - \Magento\Catalog\Model\Product\Copier $productCopier + \Magento\Catalog\Model\Product\Copier $productCopier, + \Psr\Log\LoggerInterface $logger = null ) { $this->productCopier = $productCopier; + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(\Psr\Log\LoggerInterface::class); parent::__construct($context, $productBuilder); } @@ -46,7 +59,7 @@ public function execute() $this->messageManager->addSuccessMessage(__('You duplicated the product.')); $resultRedirect->setPath('catalog/*/edit', ['_current' => true, 'id' => $newProduct->getId()]); } catch (\Exception $e) { - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->logger->critical($e); $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('catalog/*/edit', ['_current' => true]); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php index c31ceabcda655..d6781be1edc01 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php @@ -1,13 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; +/** + * Edit product + */ class Edit extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** @@ -22,18 +26,27 @@ class Edit extends \Magento\Catalog\Controller\Adminhtml\Product implements Http */ protected $resultPageFactory; + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { parent::__construct($context, $productBuilder); $this->resultPageFactory = $resultPageFactory; + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(\Magento\Store\Model\StoreManagerInterface::class); } /** @@ -43,15 +56,13 @@ public function __construct( */ public function execute() { - /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ - $storeManager = $this->_objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); $storeId = (int) $this->getRequest()->getParam('store', 0); - $store = $storeManager->getStore($storeId); - $storeManager->setCurrentStore($store->getCode()); + $store = $this->storeManager->getStore($storeId); + $this->storeManager->setCurrentStore($store->getCode()); $productId = (int) $this->getRequest()->getParam('id'); $product = $this->productBuilder->build($this->getRequest()); - if (($productId && !$product->getEntityId())) { + if ($productId && !$product->getEntityId()) { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $this->messageManager->addErrorMessage(__('This product doesn\'t exist.')); @@ -72,9 +83,8 @@ public function execute() $resultPage->getConfig()->getTitle()->prepend(__('Products')); $resultPage->getConfig()->getTitle()->prepend($product->getName()); - if (!$this->_objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->isSingleStoreMode() - && - ($switchBlock = $resultPage->getLayout()->getBlock('store_switcher')) + if (!$this->storeManager->isSingleStoreMode() + && ($switchBlock = $resultPage->getLayout()->getBlock('store_switcher')) ) { $switchBlock->setDefaultStoreName(__('Default Values')) ->setWebsiteIds($product->getWebsiteIds()) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php index ff7311e931755..d43b313c43b3e 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,7 +7,12 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +/** + * Class Upload + */ class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** @@ -23,19 +27,58 @@ class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterf */ protected $resultRawFactory; + /** + * @var array + */ + private $allowedMimeTypes = [ + 'jpg' => 'image/jpg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/png', + 'png' => 'image/gif' + ]; + + /** + * @var \Magento\Framework\Image\AdapterFactory + */ + private $adapterFactory; + + /** + * @var \Magento\Framework\Filesystem + */ + private $filesystem; + + /** + * @var \Magento\Catalog\Model\Product\Media\Config + */ + private $productMediaConfig; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + * @param \Magento\Framework\Image\AdapterFactory $adapterFactory + * @param \Magento\Framework\Filesystem $filesystem + * @param \Magento\Catalog\Model\Product\Media\Config $productMediaConfig */ public function __construct( \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, + \Magento\Framework\Image\AdapterFactory $adapterFactory = null, + \Magento\Framework\Filesystem $filesystem = null, + \Magento\Catalog\Model\Product\Media\Config $productMediaConfig = null ) { parent::__construct($context); $this->resultRawFactory = $resultRawFactory; + $this->adapterFactory = $adapterFactory ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Image\AdapterFactory::class); + $this->filesystem = $filesystem ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Filesystem::class); + $this->productMediaConfig = $productMediaConfig ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Model\Product\Media\Config::class); } /** + * Upload image(s) to the product gallery. + * * @return \Magento\Framework\Controller\Result\Raw */ public function execute() @@ -45,17 +88,15 @@ public function execute() \Magento\MediaStorage\Model\File\Uploader::class, ['fileId' => 'image'] ); - $uploader->setAllowedExtensions(['jpg', 'jpeg', 'gif', 'png']); - /** @var \Magento\Framework\Image\Adapter\AdapterInterface $imageAdapter */ - $imageAdapter = $this->_objectManager->get(\Magento\Framework\Image\AdapterFactory::class)->create(); + $uploader->setAllowedExtensions($this->getAllowedExtensions()); + $imageAdapter = $this->adapterFactory->create(); $uploader->addValidateCallback('catalog_product_image', $imageAdapter, 'validateUploadFile'); $uploader->setAllowRenameFiles(true); $uploader->setFilesDispersion(true); - /** @var \Magento\Framework\Filesystem\Directory\Read $mediaDirectory */ - $mediaDirectory = $this->_objectManager->get(\Magento\Framework\Filesystem::class) - ->getDirectoryRead(DirectoryList::MEDIA); - $config = $this->_objectManager->get(\Magento\Catalog\Model\Product\Media\Config::class); - $result = $uploader->save($mediaDirectory->getAbsolutePath($config->getBaseTmpMediaPath())); + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $result = $uploader->save( + $mediaDirectory->getAbsolutePath($this->productMediaConfig->getBaseTmpMediaPath()) + ); $this->_eventManager->dispatch( 'catalog_product_gallery_upload_image_after', @@ -65,8 +106,7 @@ public function execute() unset($result['tmp_name']); unset($result['path']); - $result['url'] = $this->_objectManager->get(\Magento\Catalog\Model\Product\Media\Config::class) - ->getTmpMediaUrl($result['file']); + $result['url'] = $this->productMediaConfig->getTmpMediaUrl($result['file']); $result['file'] = $result['file'] . '.tmp'; } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; @@ -78,4 +118,14 @@ public function execute() $response->setContents(json_encode($result)); return $response; } + + /** + * Get the set of allowed file extensions. + * + * @return array + */ + private function getAllowedExtensions() + { + return array_keys($this->allowedMimeTypes); + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index f11d16755ef0d..62bcb1c8cd6d7 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -6,17 +6,25 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; +use DateTime; +use Magento\Backend\Helper\Js; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory; +use Magento\Catalog\Api\Data\ProductLinkTypeInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository; -use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeDefaultValueFilter; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; use Magento\Catalog\Model\Product\Link\Resolver as LinkResolver; use Magento\Catalog\Model\Product\LinkTypeProvider; use Magento\Framework\App\ObjectManager; -use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Locale\FormatInterface; +use Magento\Framework\Stdlib\DateTime\Filter\Date; +use Magento\Store\Model\StoreManagerInterface; +use Zend_Filter_Input; +use Magento\Catalog\Model\Product\Authorization as ProductAuthorization; /** * Product helper @@ -28,12 +36,12 @@ class Helper { /** - * @var \Magento\Framework\App\RequestInterface + * @var RequestInterface */ protected $request; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; @@ -43,12 +51,12 @@ class Helper protected $stockFilter; /** - * @var \Magento\Backend\Helper\Js + * @var Js */ protected $jsHelper; /** - * @var \Magento\Framework\Stdlib\DateTime\Filter\Date + * @var Date * @deprecated 101.0.0 */ protected $dateFilter; @@ -96,34 +104,48 @@ class Helper */ private $attributeFilter; + /** + * @var ProductAuthorization + */ + private $productAuthorization; + + /** + * @var FormatInterface + */ + private $localeFormat; + /** * Constructor * - * @param \Magento\Framework\App\RequestInterface $request - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param RequestInterface $request + * @param StoreManagerInterface $storeManager * @param StockDataFilter $stockFilter * @param ProductLinks $productLinks - * @param \Magento\Backend\Helper\Js $jsHelper - * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter + * @param Js $jsHelper + * @param Date $dateFilter * @param CustomOptionFactory|null $customOptionFactory * @param ProductLinkFactory|null $productLinkFactory * @param ProductRepositoryInterface|null $productRepository * @param LinkTypeProvider|null $linkTypeProvider * @param AttributeFilter|null $attributeFilter + * @param FormatInterface|null $localeFormat + * @param ProductAuthorization|null $productAuthorization * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\RequestInterface $request, - \Magento\Store\Model\StoreManagerInterface $storeManager, + RequestInterface $request, + StoreManagerInterface $storeManager, StockDataFilter $stockFilter, - \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks, - \Magento\Backend\Helper\Js $jsHelper, - \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, + ProductLinks $productLinks, + Js $jsHelper, + Date $dateFilter, CustomOptionFactory $customOptionFactory = null, ProductLinkFactory $productLinkFactory = null, ProductRepositoryInterface $productRepository = null, LinkTypeProvider $linkTypeProvider = null, - AttributeFilter $attributeFilter = null + AttributeFilter $attributeFilter = null, + FormatInterface $localeFormat = null, + ?ProductAuthorization $productAuthorization = null ) { $this->request = $request; $this->storeManager = $storeManager; @@ -132,26 +154,28 @@ public function __construct( $this->jsHelper = $jsHelper; $this->dateFilter = $dateFilter; - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->customOptionFactory = $customOptionFactory ?: $objectManager->get(CustomOptionFactory::class); $this->productLinkFactory = $productLinkFactory ?: $objectManager->get(ProductLinkFactory::class); $this->productRepository = $productRepository ?: $objectManager->get(ProductRepositoryInterface::class); $this->linkTypeProvider = $linkTypeProvider ?: $objectManager->get(LinkTypeProvider::class); $this->attributeFilter = $attributeFilter ?: $objectManager->get(AttributeFilter::class); + $this->localeFormat = $localeFormat ?: $objectManager->get(FormatInterface::class); + $this->productAuthorization = $productAuthorization ?? $objectManager->get(ProductAuthorization::class); } /** * Initialize product from data * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param array $productData - * @return \Magento\Catalog\Model\Product + * @return Product * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @since 101.0.0 */ - public function initializeFromData(\Magento\Catalog\Model\Product $product, array $productData) + public function initializeFromData(Product $product, array $productData) { unset($productData['custom_attributes'], $productData['extension_attributes']); @@ -190,7 +214,7 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra } } - $inputFilter = new \Zend_Filter_Input($dateFieldFilters, [], $productData); + $inputFilter = new Zend_Filter_Input($dateFieldFilters, [], $productData); $productData = $inputFilter->getUnescaped(); if (isset($productData['options'])) { @@ -201,7 +225,7 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra } $productData['tier_price'] = isset($productData['tier_price']) ? $productData['tier_price'] : []; - $useDefaults = (array)$this->request->getPost('use_default', []); + $useDefaults = (array) $this->request->getPost('use_default', []); $productData = $this->attributeFilter->prepareProductAttributes($product, $productData, $useDefaults); $product->addData($productData); @@ -222,24 +246,27 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra /** * Initialize product before saving * - * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Catalog\Model\Product + * @param Product $product + * @return Product */ - public function initialize(\Magento\Catalog\Model\Product $product) + public function initialize(Product $product) { $productData = $this->request->getPost('product', []); - return $this->initializeFromData($product, $productData); + $product = $this->initializeFromData($product, $productData); + $this->productAuthorization->authorizeSavingOf($product); + + return $product; } /** * Setting product links * - * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Catalog\Model\Product + * @param Product $product + * @return Product * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @since 101.0.0 */ - protected function setProductLinks(\Magento\Catalog\Model\Product $product) + protected function setProductLinks(Product $product) { $links = $this->getLinkResolver()->getLinks(); @@ -249,7 +276,7 @@ protected function setProductLinks(\Magento\Catalog\Model\Product $product) $productLinks = $product->getProductLinks(); $linkTypes = []; - /** @var \Magento\Catalog\Api\Data\ProductLinkTypeInterface $linkTypeObject */ + /** @var ProductLinkTypeInterface $linkTypeObject */ foreach ($this->linkTypeProvider->getItems() as $linkTypeObject) { $linkTypes[$linkTypeObject->getName()] = $product->getData($linkTypeObject->getName() . '_readonly'); } @@ -261,7 +288,7 @@ protected function setProductLinks(\Magento\Catalog\Model\Product $product) foreach ($linkTypes as $linkType => $readonly) { if (isset($links[$linkType]) && !$readonly) { - foreach ((array)$links[$linkType] as $linkData) { + foreach ((array) $links[$linkType] as $linkData) { if (empty($linkData['id'])) { continue; } @@ -271,7 +298,7 @@ protected function setProductLinks(\Magento\Catalog\Model\Product $product) $link->setSku($product->getSku()) ->setLinkedProductSku($linkProduct->getSku()) ->setLinkType($linkType) - ->setPosition(isset($linkData['position']) ? (int)$linkData['position'] : 0); + ->setPosition(isset($linkData['position']) ? (int) $linkData['position'] : 0); $productLinks[] = $link; } } @@ -377,6 +404,7 @@ private function getLinkResolver() if (!is_object($this->linkResolver)) { $this->linkResolver = ObjectManager::getInstance()->get(LinkResolver::class); } + return $this->linkResolver; } @@ -389,9 +417,10 @@ private function getLinkResolver() private function getDateTimeFilter() { if ($this->dateTimeFilter === null) { - $this->dateTimeFilter = \Magento\Framework\App\ObjectManager::getInstance() + $this->dateTimeFilter = ObjectManager::getInstance() ->get(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class); } + return $this->dateTimeFilter; } @@ -407,7 +436,7 @@ private function getDateTimeFilter() private function filterWebsiteIds($websiteIds) { if (!$this->storeManager->isSingleStoreMode()) { - $websiteIds = array_filter((array)$websiteIds); + $websiteIds = array_filter((array) $websiteIds); } else { $websiteIds[$this->storeManager->getWebsite(true)->getId()] = 1; } @@ -448,9 +477,17 @@ private function fillProductOptions(Product $product, array $productOptions) } if (isset($customOptionData['values'])) { - $customOptionData['values'] = array_filter($customOptionData['values'], function ($valueData) { - return empty($valueData['is_delete']); - }); + $customOptionData['values'] = array_filter( + $customOptionData['values'], + function ($valueData) { + return empty($valueData['is_delete']); + } + ); + } + + if (isset($customOptionData['price'])) { + // Make sure we're working with a number here and no localized value. + $customOptionData['price'] = $this->localeFormat->getNumber($customOptionData['price']); } $customOption = $this->customOptionFactory->create(['data' => $customOptionData]); @@ -471,7 +508,7 @@ private function convertSpecialFromDateStringToObject($productData) { if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') { $productData['special_from_date'] = $this->getDateTimeFilter()->filter($productData['special_from_date']); - $productData['special_from_date'] = new \DateTime($productData['special_from_date']); + $productData['special_from_date'] = new DateTime($productData['special_from_date']); } return $productData; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php index 9d7273fb3f23c..b6c4db6c64382 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php @@ -7,8 +7,8 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; use Magento\Ui\Component\MassAction\Filter; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -37,22 +37,31 @@ class MassStatus extends \Magento\Catalog\Controller\Adminhtml\Product implement protected $collectionFactory; /** - * @param Action\Context $context + * @var \Magento\Catalog\Model\Product\Action + */ + private $productAction; + + /** + * @param \Magento\Backend\App\Action\Context $context * @param Builder $productBuilder * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor * @param Filter $filter * @param CollectionFactory $collectionFactory + * @param \Magento\Catalog\Model\Product\Action $productAction */ public function __construct( \Magento\Backend\App\Action\Context $context, Product\Builder $productBuilder, \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, Filter $filter, - CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + \Magento\Catalog\Model\Product\Action $productAction = null ) { $this->filter = $filter; $this->collectionFactory = $collectionFactory; $this->_productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->productAction = $productAction ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Model\Product\Action::class); parent::__construct($context, $productBuilder); } @@ -94,8 +103,7 @@ public function execute() try { $this->_validateMassStatus($productIds, $status); - $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class) - ->updateAttributes($productIds, ['status' => $status], (int) $storeId); + $this->productAction->updateAttributes($productIds, ['status' => $status], (int) $storeId); $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been updated.', count($productIds)) ); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index 825d0ee032d6c..5c3e27334cb66 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,6 +10,7 @@ use Magento\Backend\App\Action; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Request\DataPersistorInterface; @@ -56,12 +56,12 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements Http private $storeManager; /** - * @var \Magento\Framework\Escaper|null + * @var \Magento\Framework\Escaper */ private $escaper; /** - * @var null|\Psr\Log\LoggerInterface + * @var \Psr\Log\LoggerInterface */ private $logger; @@ -74,8 +74,11 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements Http * @param \Magento\Catalog\Model\Product\Copier $productCopier * @param \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository - * @param \Magento\Framework\Escaper|null $escaper - * @param \Psr\Log\LoggerInterface|null $logger + * @param \Magento\Framework\Escaper $escaper + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement + * @param StoreManagerInterface $storeManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -85,15 +88,23 @@ public function __construct( \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager, \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Framework\Escaper $escaper = null, - \Psr\Log\LoggerInterface $logger = null + \Psr\Log\LoggerInterface $logger = null, + \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement = null, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { + parent::__construct($context, $productBuilder); $this->initializationHelper = $initializationHelper; $this->productCopier = $productCopier; $this->productTypeManager = $productTypeManager; $this->productRepository = $productRepository; - parent::__construct($context, $productBuilder); - $this->escaper = $escaper ?? $this->_objectManager->get(\Magento\Framework\Escaper::class); - $this->logger = $logger ?? $this->_objectManager->get(\Psr\Log\LoggerInterface::class); + $this->escaper = $escaper ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Escaper::class); + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(\Psr\Log\LoggerInterface::class); + $this->categoryLinkManagement = $categoryLinkManagement ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(\Magento\Store\Model\StoreManagerInterface::class); } /** @@ -106,8 +117,8 @@ public function __construct( public function execute() { $storeId = $this->getRequest()->getParam('store', 0); - $store = $this->getStoreManager()->getStore($storeId); - $this->getStoreManager()->setCurrentStore($store->getCode()); + $store = $this->storeManager->getStore($storeId); + $this->storeManager->setCurrentStore($store->getCode()); $redirectBack = $this->getRequest()->getParam('back', false); $productId = $this->getRequest()->getParam('id'); $resultRedirect = $this->resultRedirectFactory->create(); @@ -130,7 +141,7 @@ public function execute() $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); - $this->getCategoryLinkManagement()->assignProductToCategories( + $this->categoryLinkManagement->assignProductToCategories( $product->getSku(), $product->getCategoryIds() ); @@ -236,11 +247,9 @@ private function handleImageRemoveError($postData, $productId) /** * Do copying data to stores * - * If the 'copy_from' field is not specified in the input data, - * the store fallback mechanism will automatically take the admin store's default value. - * * @param array $data * @param int $productId + * * @return void */ protected function copyToStores($data, $productId) @@ -250,19 +259,7 @@ protected function copyToStores($data, $productId) if (isset($data['product']['website_ids'][$websiteId]) && (bool)$data['product']['website_ids'][$websiteId]) { foreach ($group as $store) { - if (isset($store['copy_from'])) { - $copyFrom = $store['copy_from']; - $copyTo = (isset($store['copy_to'])) ? $store['copy_to'] : 0; - if ($copyTo) { - $this->_objectManager->create(\Magento\Catalog\Model\Product::class) - ->setStoreId($copyFrom) - ->load($productId) - ->setStoreId($copyTo) - ->setCanSaveCustomOptions($data['can_save_custom_options']) - ->setCopyFromView(true) - ->save(); - } - } + $this->copyToStore($data, $productId, $store); } } } @@ -270,32 +267,30 @@ protected function copyToStores($data, $productId) } /** - * Get categoryLinkManagement in a backward compatible way. + * Do copying data to stores * - * @return \Magento\Catalog\Api\CategoryLinkManagementInterface - */ - private function getCategoryLinkManagement() - { - if (null === $this->categoryLinkManagement) { - $this->categoryLinkManagement = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); - } - return $this->categoryLinkManagement; - } - - /** - * Get storeManager in a backward compatible way. + * If the 'copy_from' field is not specified in the input data, + * the store fallback mechanism will automatically take the admin store's default value. * - * @return StoreManagerInterface - * @deprecated 101.0.0 + * @param array $data + * @param int $productId + * @param array $store */ - private function getStoreManager() + private function copyToStore($data, $productId, $store) { - if (null === $this->storeManager) { - $this->storeManager = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreManagerInterface::class); + if (isset($store['copy_from'])) { + $copyFrom = $store['copy_from']; + $copyTo = (isset($store['copy_to'])) ? $store['copy_to'] : 0; + if ($copyTo) { + $this->_objectManager->create(\Magento\Catalog\Model\Product::class) + ->setStoreId($copyFrom) + ->load($productId) + ->setStoreId($copyTo) + ->setCanSaveCustomOptions($data['can_save_custom_options']) + ->setCopyFromView(true) + ->save(); + } } - return $this->storeManager; } /** diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php index 6f6870cb0849f..89d2c1b8a066e 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php @@ -6,45 +6,63 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\Registry; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\Action\HttpGetActionInterface; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface +/** + * Edit attribute set controller. + */ +class Edit extends Set implements HttpGetActionInterface { /** - * @var \Magento\Framework\View\Result\PageFactory + * @var PageFactory */ protected $resultPageFactory; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @var AttributeSetRepositoryInterface + */ + private $attributeSetRepository; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param PageFactory $resultPageFactory + * @param AttributeSetRepositoryInterface $attributeSetRepository */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + Context $context, + Registry $coreRegistry, + PageFactory $resultPageFactory, + AttributeSetRepositoryInterface $attributeSetRepository = null ) { parent::__construct($context, $coreRegistry); $this->resultPageFactory = $resultPageFactory; + $this->attributeSetRepository = $attributeSetRepository ?: + ObjectManager::getInstance()->get(AttributeSetRepositoryInterface::class); } /** - * @return \Magento\Backend\Model\View\Result\Page + * @inheritdoc */ public function execute() { $this->_setTypeId(); - $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) - ->load($this->getRequest()->getParam('id')); - + $attributeSet = $this->attributeSetRepository->get($this->getRequest()->getParam('id')); if (!$attributeSet->getId()) { return $this->resultRedirectFactory->create()->setPath('catalog/*/index'); } - $this->_coreRegistry->register('current_attribute_set', $attributeSet); - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ + /** @var Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $resultPage->setActiveMenu('Magento_Catalog::catalog_attributes_sets'); $resultPage->getConfig()->getTitle()->prepend(__('Attribute Sets')); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Wysiwyg.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Wysiwyg.php index c0bb9f60c1878..e7d576e5e941f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Wysiwyg.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Wysiwyg.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class Wysiwyg extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; + +/** + * Class Wysiwyg + */ +class Wysiwyg extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory @@ -18,21 +23,30 @@ class Wysiwyg extends \Magento\Catalog\Controller\Adminhtml\Product */ protected $layoutFactory; + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder, \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { parent::__construct($context, $productBuilder); $this->resultRawFactory = $resultRawFactory; $this->layoutFactory = $layoutFactory; + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(\Magento\Store\Model\StoreManagerInterface::class); } /** @@ -42,10 +56,11 @@ public function __construct( */ public function execute() { + // @codingStandardsIgnoreStart $elementId = $this->getRequest()->getParam('element_id', md5(microtime())); + // @codingStandardsIgnoreEnd $storeId = $this->getRequest()->getParam('store_id', 0); - $storeMediaUrl = $this->_objectManager->get(\Magento\Store\Model\StoreManagerInterface::class) - ->getStore($storeId) + $storeMediaUrl = $this->storeManager->getStore($storeId) ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA); $content = $this->layoutFactory->create() diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index da3d99a8d2745..eea448e0fdb0b 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Category; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Helper\Category as CategoryHelper; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Design; use Magento\Catalog\Model\Layer\Resolver; @@ -18,6 +19,7 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\ForwardFactory; use Magento\Framework\Controller\ResultInterface; use Magento\Framework\DataObject; @@ -28,6 +30,7 @@ use Magento\Framework\View\Result\PageFactory; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; /** * View a category on storefront. Needs to be accessible by POST because of the store switching. @@ -94,6 +97,21 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter */ private $toolbarMemorizer; + /** + * @var LayoutUpdateManager + */ + private $customLayoutManager; + + /** + * @var CategoryHelper + */ + private $categoryHelper; + + /** + * @var LoggerInterface + */ + private $logger; + /** * Constructor * @@ -108,6 +126,9 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter * @param Resolver $layerResolver * @param CategoryRepositoryInterface $categoryRepository * @param ToolbarMemorizer|null $toolbarMemorizer + * @param LayoutUpdateManager|null $layoutUpdateManager + * @param CategoryHelper $categoryHelper + * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -121,7 +142,10 @@ public function __construct( ForwardFactory $resultForwardFactory, Resolver $layerResolver, CategoryRepositoryInterface $categoryRepository, - ToolbarMemorizer $toolbarMemorizer = null + ToolbarMemorizer $toolbarMemorizer = null, + ?LayoutUpdateManager $layoutUpdateManager = null, + CategoryHelper $categoryHelper = null, + LoggerInterface $logger = null ) { parent::__construct($context); $this->_storeManager = $storeManager; @@ -133,7 +157,13 @@ public function __construct( $this->resultForwardFactory = $resultForwardFactory; $this->layerResolver = $layerResolver; $this->categoryRepository = $categoryRepository; - $this->toolbarMemorizer = $toolbarMemorizer ?: $context->getObjectManager()->get(ToolbarMemorizer::class); + $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()->get(ToolbarMemorizer::class); + $this->customLayoutManager = $layoutUpdateManager + ?? ObjectManager::getInstance()->get(LayoutUpdateManager::class); + $this->categoryHelper = $categoryHelper ?: ObjectManager::getInstance() + ->get(CategoryHelper::class); + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** @@ -153,7 +183,7 @@ protected function _initCategory() } catch (NoSuchEntityException $e) { return false; } - if (!$this->_objectManager->get(\Magento\Catalog\Helper\Category::class)->canShow($category)) { + if (!$this->categoryHelper->canShow($category)) { return false; } $this->_catalogSession->setLastVisitedCategoryId($category->getId()); @@ -165,7 +195,7 @@ protected function _initCategory() ['category' => $category, 'controller_action' => $this] ); } catch (LocalizedException $e) { - $this->_objectManager->get(LoggerInterface::class)->critical($e); + $this->logger->critical($e); return false; } @@ -258,5 +288,10 @@ private function applyLayoutUpdates( $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); } } + + //Selected files + if ($settings->getPageLayoutHandles()) { + $page->addPageLayoutHandles($settings->getPageLayoutHandles()); + } } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php index f5c3171a3fe90..8b854361fd4ef 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php @@ -6,14 +6,72 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Result\PageFactory; /** * Add item to compare list action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Add extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface { + /** + * @var AddToCompareAvailability + */ + private $compareAvailability; + + /** + * @param \Magento\Framework\App\Action\Context $context + * @param \Magento\Catalog\Model\Product\Compare\ItemFactory $compareItemFactory + * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory $itemCollectionFactory + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Customer\Model\Visitor $customerVisitor + * @param \Magento\Catalog\Model\Product\Compare\ListCompare $catalogProductCompareList + * @param \Magento\Catalog\Model\Session $catalogSession + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Validator $formKeyValidator + * @param PageFactory $resultPageFactory + * @param ProductRepositoryInterface $productRepository + * @param AddToCompareAvailability|null $compareAvailability + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\App\Action\Context $context, + \Magento\Catalog\Model\Product\Compare\ItemFactory $compareItemFactory, + \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory $itemCollectionFactory, + \Magento\Customer\Model\Session $customerSession, + \Magento\Customer\Model\Visitor $customerVisitor, + \Magento\Catalog\Model\Product\Compare\ListCompare $catalogProductCompareList, + \Magento\Catalog\Model\Session $catalogSession, + \Magento\Store\Model\StoreManagerInterface $storeManager, + Validator $formKeyValidator, + PageFactory $resultPageFactory, + ProductRepositoryInterface $productRepository, + AddToCompareAvailability $compareAvailability = null + ) { + parent::__construct( + $context, + $compareItemFactory, + $itemCollectionFactory, + $customerSession, + $customerVisitor, + $catalogProductCompareList, + $catalogSession, + $storeManager, + $formKeyValidator, + $resultPageFactory, + $productRepository + ); + + $this->compareAvailability = $compareAvailability + ?: $this->_objectManager->get(AddToCompareAvailability::class); + } + /** * Add item to compare list. * @@ -36,7 +94,7 @@ public function execute() $product = null; } - if ($product && $product->isSalable()) { + if ($product && $this->compareAvailability->isAvailableForCompare($product)) { $this->_catalogProductCompareList->addProduct($product); $productName = $this->_objectManager->get( \Magento\Framework\Escaper::class diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index acf0f1b754c12..f5d56dc9e6b0e 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -6,6 +6,7 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; @@ -17,12 +18,13 @@ class Remove extends \Magento\Catalog\Controller\Product\Compare implements Http /** * Remove item from compare list. * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { $productId = (int)$this->getRequest()->getParam('product'); - if ($productId) { + if ($this->_formKeyValidator->validate($this->getRequest()) && $productId) { $storeId = $this->_storeManager->getStore()->getId(); try { /** @var \Magento\Catalog\Model\Product $product */ @@ -31,7 +33,7 @@ public function execute() $product = null; } - if ($product && $product->isSalable()) { + if ($product && (int)$product->getStatus() !== Status::STATUS_DISABLED) { /** @var $item \Magento\Catalog\Model\Product\Compare\Item */ $item = $this->_compareItemFactory->create(); if ($this->_customerSession->isLoggedIn()) { diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php index 024123e15150d..570b8f541b762 100644 --- a/app/code/Magento/Catalog/Controller/Product/View.php +++ b/app/code/Magento/Catalog/Controller/Product/View.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,11 +8,14 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Result\PageFactory; use Magento\Catalog\Controller\Product as ProductAction; /** - * View a product on storefront. Needs to be accessible by POST because of the store switching. + * View a product on storefront. Needs to be accessible by POST because of the store switching + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class View extends ProductAction implements HttpGetActionInterface, HttpPostActionInterface { @@ -32,6 +34,16 @@ class View extends ProductAction implements HttpGetActionInterface, HttpPostActi */ protected $resultPageFactory; + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var \Magento\Framework\Json\Helper\Data + */ + private $jsonHelper; + /** * Constructor * @@ -39,17 +51,25 @@ class View extends ProductAction implements HttpGetActionInterface, HttpPostActi * @param \Magento\Catalog\Helper\Product\View $viewHelper * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory * @param PageFactory $resultPageFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Json\Helper\Data $jsonHelper */ public function __construct( Context $context, \Magento\Catalog\Helper\Product\View $viewHelper, \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory, - PageFactory $resultPageFactory + PageFactory $resultPageFactory, + \Psr\Log\LoggerInterface $logger = null, + \Magento\Framework\Json\Helper\Data $jsonHelper = null ) { + parent::__construct($context); $this->viewHelper = $viewHelper; $this->resultForwardFactory = $resultForwardFactory; $this->resultPageFactory = $resultPageFactory; - parent::__construct($context); + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(\Psr\Log\LoggerInterface::class); + $this->jsonHelper = $jsonHelper ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Json\Helper\Data::class); } /** @@ -84,21 +104,23 @@ public function execute() if ($this->getRequest()->isPost() && $this->getRequest()->getParam(self::PARAM_NAME_URL_ENCODED)) { $product = $this->_initProduct(); - + if (!$product) { return $this->noProductRedirect(); } - + if ($specifyOptions) { $notice = $product->getTypeInstance()->getSpecifyOptionMessage(); $this->messageManager->addNoticeMessage($notice); } - + if ($this->getRequest()->isAjax()) { $this->getResponse()->representJson( - $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode([ - 'backUrl' => $this->_redirect->getRedirectUrl() - ]) + $this->jsonHelper->jsonEncode( + [ + 'backUrl' => $this->_redirect->getRedirectUrl() + ] + ) ); return; } @@ -120,7 +142,7 @@ public function execute() } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { return $this->noProductRedirect(); } catch (\Exception $e) { - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->logger->critical($e); $resultForward = $this->resultForwardFactory->create(); $resultForward->forward('noroute'); return $resultForward; diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 9b8d0ad75a8c9..110b798df9df9 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -213,31 +213,29 @@ protected function setImageProperties() // Set 'keep frame' flag $frame = $this->getFrame(); - if (!empty($frame)) { - $this->_getModel()->setKeepFrame($frame); - } + $this->_getModel()->setKeepFrame($frame); // Set 'constrain only' flag $constrain = $this->getAttribute('constrain'); - if (!empty($constrain)) { + if (null !== $constrain) { $this->_getModel()->setConstrainOnly($constrain); } // Set 'keep aspect ratio' flag $aspectRatio = $this->getAttribute('aspect_ratio'); - if (!empty($aspectRatio)) { + if (null !== $aspectRatio) { $this->_getModel()->setKeepAspectRatio($aspectRatio); } // Set 'transparency' flag $transparency = $this->getAttribute('transparency'); - if (!empty($transparency)) { + if (null !== $transparency) { $this->_getModel()->setKeepTransparency($transparency); } // Set background color $background = $this->getAttribute('background'); - if (!empty($background)) { + if (null !== $background) { $this->_getModel()->setBackgroundColor($background); } diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php index 33e261dc353b4..93b67965e7234 100644 --- a/app/code/Magento/Catalog/Helper/Output.php +++ b/app/code/Magento/Catalog/Helper/Output.php @@ -9,9 +9,21 @@ use Magento\Catalog\Model\Category as ModelCategory; use Magento\Catalog\Model\Product as ModelProduct; +use Magento\Eav\Model\Config; +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filter\Template; +use function is_object; +use function method_exists; +use function preg_match; +use function strtolower; -class Output extends \Magento\Framework\App\Helper\AbstractHelper +/** + * Html output + */ +class Output extends AbstractHelper { /** * Array of existing handlers @@ -37,12 +49,12 @@ class Output extends \Magento\Framework\App\Helper\AbstractHelper /** * Eav config * - * @var \Magento\Eav\Model\Config + * @var Config */ protected $_eavConfig; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $_escaper; @@ -53,27 +65,32 @@ class Output extends \Magento\Framework\App\Helper\AbstractHelper /** * Output constructor. - * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Eav\Model\Config $eavConfig + * @param Context $context + * @param Config $eavConfig * @param Data $catalogData - * @param \Magento\Framework\Escaper $escaper + * @param Escaper $escaper * @param array $directivePatterns + * @param array $handlers */ public function __construct( - \Magento\Framework\App\Helper\Context $context, - \Magento\Eav\Model\Config $eavConfig, + Context $context, + Config $eavConfig, Data $catalogData, - \Magento\Framework\Escaper $escaper, - $directivePatterns = [] + Escaper $escaper, + $directivePatterns = [], + array $handlers = [] ) { $this->_eavConfig = $eavConfig; $this->_catalogData = $catalogData; $this->_escaper = $escaper; $this->directivePatterns = $directivePatterns; + $this->_handlers = $handlers; parent::__construct($context); } /** + * Return template processor + * * @return Template */ protected function _getTemplateProcessor() @@ -115,8 +132,7 @@ public function addHandler($method, $handler) */ public function getHandlers($method) { - $method = strtolower($method); - return $this->_handlers[$method] ?? []; + return $this->_handlers[strtolower($method)] ?? []; } /** @@ -145,21 +161,21 @@ public function process($method, $result, $params) * @param string $attributeName * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function productAttribute($product, $attributeHtml, $attributeName) { $attribute = $this->_eavConfig->getAttribute(ModelProduct::ENTITY, $attributeName); if ($attribute && $attribute->getId() && - $attribute->getFrontendInput() != 'media_image' && + $attribute->getFrontendInput() !== 'media_image' && (!$attribute->getIsHtmlAllowedOnFront() && !$attribute->getIsWysiwygEnabled()) ) { - if ($attribute->getFrontendInput() != 'price') { + if ($attribute->getFrontendInput() !== 'price') { $attributeHtml = $this->_escaper->escapeHtml($attributeHtml); } - if ($attribute->getFrontendInput() == 'textarea') { + if ($attribute->getFrontendInput() === 'textarea') { $attributeHtml = nl2br($attributeHtml); } } @@ -187,14 +203,14 @@ public function productAttribute($product, $attributeHtml, $attributeName) * @param string $attributeHtml * @param string $attributeName * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function categoryAttribute($category, $attributeHtml, $attributeName) { $attribute = $this->_eavConfig->getAttribute(ModelCategory::ENTITY, $attributeName); if ($attribute && - $attribute->getFrontendInput() != 'image' && + $attribute->getFrontendInput() !== 'image' && (!$attribute->getIsHtmlAllowedOnFront() && !$attribute->getIsWysiwygEnabled()) ) { diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php index 74f40a18971d5..cf5b15cadc997 100644 --- a/app/code/Magento/Catalog/Helper/Product/View.php +++ b/app/code/Magento/Catalog/Helper/Product/View.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Helper\Product; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Result\Page as ResultPage; /** @@ -66,6 +68,11 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper */ private $string; + /** + * @var LayoutUpdateManager + */ + private $layoutUpdateManager; + /** * Constructor * @@ -78,6 +85,8 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator * @param array $messageGroups * @param \Magento\Framework\Stdlib\StringUtils|null $string + * @param LayoutUpdateManager|null $layoutUpdateManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -88,7 +97,8 @@ public function __construct( \Magento\Framework\Message\ManagerInterface $messageManager, \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, array $messageGroups = [], - \Magento\Framework\Stdlib\StringUtils $string = null + \Magento\Framework\Stdlib\StringUtils $string = null, + ?LayoutUpdateManager $layoutUpdateManager = null ) { $this->_catalogSession = $catalogSession; $this->_catalogDesign = $catalogDesign; @@ -97,8 +107,9 @@ public function __construct( $this->messageGroups = $messageGroups; $this->messageManager = $messageManager; $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; - $this->string = $string ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Stdlib\StringUtils::class); + $this->string = $string ?: ObjectManager::getInstance()->get(\Magento\Framework\Stdlib\StringUtils::class); + $this->layoutUpdateManager = $layoutUpdateManager + ?? ObjectManager::getInstance()->get(LayoutUpdateManager::class); parent::__construct($context); } @@ -203,6 +214,9 @@ public function initProductLayout(ResultPage $resultPage, $product, $params = nu } } } + if ($settings->getPageLayoutHandles()) { + $resultPage->addPageLayoutHandles($settings->getPageLayoutHandles()); + } $currentCategory = $this->_coreRegistry->registry('current_category'); $controllerClass = $this->_request->getFullActionName(); diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php new file mode 100644 index 0000000000000..1aa7ab7e5880f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Backend; + +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Framework\Exception\LocalizedException; +use Magento\Catalog\Model\AbstractModel; + +/** + * Custom layout file attribute. + */ +abstract class AbstractLayoutUpdate extends AbstractBackend +{ + public const VALUE_USE_UPDATE_XML = '__existing__'; + + public const VALUE_NO_UPDATE = '__no_update__'; + + /** + * Extract attribute value. + * + * @param AbstractModel $model + * @return mixed + */ + private function extractAttributeValue(AbstractModel $model) + { + $code = $this->getAttribute()->getAttributeCode(); + + return $model->getData($code); + } + + /** + * Compose list of available files (layout handles) for given entity. + * + * @param AbstractModel $forModel + * @return string[] + */ + abstract protected function listAvailableValues(AbstractModel $forModel): array; + + /** + * Extracts prepare attribute value to be saved. + * + * @throws LocalizedException + * @param AbstractModel $model + * @return string|null + */ + private function prepareValue(AbstractModel $model): ?string + { + $value = $this->extractAttributeValue($model); + if (!is_string($value)) { + $value = null; + } + if ($value + && $value !== self::VALUE_USE_UPDATE_XML + && $value !== self::VALUE_NO_UPDATE + && !in_array($value, $this->listAvailableValues($model), true) + ) { + throw new LocalizedException(__('Selected layout update is not available')); + } + + return $value; + } + + /** + * Set value for the object. + * + * @param string|null $value + * @param AbstractModel $forObject + * @param string|null $attrCode + * @return void + */ + private function setAttributeValue(?string $value, AbstractModel $forObject, ?string $attrCode = null): void + { + $attrCode = $attrCode ?? $this->getAttribute()->getAttributeCode(); + if ($forObject->hasData(AbstractModel::CUSTOM_ATTRIBUTES)) { + $forObject->setCustomAttribute($attrCode, $value); + } + $forObject->setData($attrCode, $value); + } + + /** + * @inheritDoc + * + * @param AbstractModel $object + */ + public function validate($object) + { + $valid = parent::validate($object); + if ($valid) { + $this->prepareValue($object); + } + + return $valid; + } + + /** + * @inheritDoc + * @param AbstractModel $object + * @throws LocalizedException + */ + public function beforeSave($object) + { + $value = $this->prepareValue($object); + if ($value && ($value === self::VALUE_NO_UPDATE || $value !== self::VALUE_USE_UPDATE_XML)) { + $this->setAttributeValue(null, $object, 'custom_layout_update'); + } + if (!$value || $value === self::VALUE_USE_UPDATE_XML || $value === self::VALUE_NO_UPDATE) { + $value = null; + } + $this->setAttributeValue($value, $object); + + return $this; + } +} diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php index a994446881189..b5aa5e2035100 100644 --- a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php @@ -5,8 +5,10 @@ */ namespace Magento\Catalog\Model\Attribute\Backend; +use Magento\Catalog\Model\AbstractModel; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; -use Magento\Eav\Model\Entity\Attribute\Exception; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; /** * Layout update attribute backend @@ -16,18 +18,15 @@ * @SuppressWarnings(PHPMD.LongVariable) * @since 100.0.2 */ -class Customlayoutupdate extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend +class Customlayoutupdate extends AbstractBackend { /** - * Layout update validator factory - * * @var ValidatorFactory + * @deprecated Is not used anymore. */ protected $_layoutUpdateValidatorFactory; /** - * Construct the custom layout update class - * * @param ValidatorFactory $layoutUpdateValidatorFactory */ public function __construct(ValidatorFactory $layoutUpdateValidatorFactory) @@ -36,31 +35,95 @@ public function __construct(ValidatorFactory $layoutUpdateValidatorFactory) } /** - * Validate the custom layout update + * Extract an attribute value. * - * @param \Magento\Framework\DataObject $object - * @return bool - * @throws Exception + * @param AbstractModel $object + * @return mixed */ - public function validate($object) + private function extractValue(AbstractModel $object) + { + $attributeCode = $attributeCode ?? $this->getAttribute()->getName(); + $value = $object->getData($attributeCode); + if (!$value || !is_string($value)) { + $value = null; + } + + return $value; + } + + /** + * Extract old attribute value. + * + * @param AbstractModel $object + * @return mixed Old value or null. + */ + private function extractOldValue(AbstractModel $object) { - $attributeName = $this->getAttribute()->getName(); - $xml = trim($object->getData($attributeName)); + if (!empty($object->getId())) { + $attr = $this->getAttribute()->getAttributeCode(); + + if ($object->getOrigData()) { + return $object->getOrigData($attr); + } - if (!$this->getAttribute()->getIsRequired() && empty($xml)) { - return true; + $oldObject = clone $object; + $oldObject->unsetData(); + $oldObject->load($object->getId()); + + return $oldObject->getData($attr); } - /** @var $validator \Magento\Framework\View\Model\Layout\Update\Validator */ - $validator = $this->_layoutUpdateValidatorFactory->create(); - if (!$validator->isValid($xml)) { - $messages = $validator->getMessages(); - //Add first message to exception - $message = array_shift($messages); - $eavExc = new Exception(__($message)); - $eavExc->setAttributeCode($attributeName); - throw $eavExc; + return null; + } + + /** + * @inheritDoc + * + * @param AbstractModel $object + */ + public function validate($object) + { + if (parent::validate($object)) { + if ($object instanceof AbstractModel) { + $value = $this->extractValue($object); + $oldValue = $this->extractOldValue($object); + if ($value && $oldValue !== $value) { + throw new LocalizedException(__('Custom layout update text cannot be changed, only removed')); + } + } } + return true; } + + /** + * Put an attribute value. + * + * @param AbstractModel $object + * @param string|null $value + * @return void + */ + private function putValue(AbstractModel $object, ?string $value): void + { + $attributeCode = $this->getAttribute()->getName(); + if ($object->hasData(AbstractModel::CUSTOM_ATTRIBUTES)) { + $object->setCustomAttribute($attributeCode, $value); + } + $object->setData($attributeCode, $value); + } + + /** + * @inheritDoc + * + * @param AbstractModel $object + * @throws LocalizedException + */ + public function beforeSave($object) + { + //Validate first, validation might have been skipped. + $this->validate($object); + $this->putValue($object, $this->extractValue($object)); + + return parent::beforeSave($object); + } } diff --git a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php index 0940ca7a234a3..cf194615b1f3b 100644 --- a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php +++ b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php @@ -81,7 +81,7 @@ public function containsValue($entityType, $entity, $attributeCode, $storeId) if ((int)$storeId === Store::DEFAULT_STORE_ID) { return false; } - if ($this->attributesValues === null) { + if (!isset($this->attributesValues[$storeId])) { $this->initAttributeValues($entityType, $entity, (int)$storeId); } @@ -110,6 +110,8 @@ public function getDefaultValues($entityType, $entity) } /** + * Init attribute values. + * * @param string $entityType * @param \Magento\Catalog\Model\AbstractModel $entity * @param int $storeId @@ -158,6 +160,8 @@ private function initAttributeValues($entityType, $entity, $storeId) } /** + * Returns entity attributes. + * * @param string $entityType * @return \Magento\Eav\Api\Data\AttributeInterface[] */ diff --git a/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php b/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php new file mode 100644 index 0000000000000..0003b9996c84b --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Source; + +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate as Backend; +use Magento\Framework\Model\AbstractExtensibleModel; + +/** + * List of layout updates available for a catalog entity. + */ +abstract class AbstractLayoutUpdate extends AbstractSource implements SpecificSourceInterface +{ + /** + * @var string[] + */ + private $optionsText; + + /** + * @inheritDoc + */ + public function getAllOptions() + { + $default = Backend::VALUE_NO_UPDATE; + $defaultText = 'No update'; + $this->optionsText[$default] = $defaultText; + + return [['label' => $defaultText, 'value' => $default]]; + } + + /** + * @inheritDoc + */ + public function getOptionText($value) + { + if (is_scalar($value) && array_key_exists($value, $this->optionsText)) { + return $this->optionsText[$value]; + } + + return false; + } + + /** + * Extract attribute value. + * + * @param CustomAttributesDataInterface|AbstractExtensibleModel $entity + * @return mixed + */ + private function extractAttributeValue(CustomAttributesDataInterface $entity) + { + $attrCode = 'custom_layout_update'; + if ($entity instanceof AbstractExtensibleModel + && !$entity->hasData(CustomAttributesDataInterface::CUSTOM_ATTRIBUTES) + ) { + //Custom attributes were not loaded yet, using data array + return $entity->getData($attrCode); + } + //Fallback to customAttribute method + $attr = $entity->getCustomAttribute($attrCode); + + return $attr ? $attr->getValue() : null; + } + + /** + * List available layout update options for the entity. + * + * @param CustomAttributesDataInterface $entity + * @return string[] + */ + abstract protected function listAvailableOptions(CustomAttributesDataInterface $entity): array; + + /** + * @inheritDoc + * + * @param CustomAttributesDataInterface|AbstractExtensibleModel $entity + */ + public function getOptionsFor(CustomAttributesDataInterface $entity): array + { + $options = $this->getAllOptions(); + if ($this->extractAttributeValue($entity)) { + $existingValue = Backend::VALUE_USE_UPDATE_XML; + $existingLabel = 'Use existing'; + $options[] = ['label' => $existingLabel, 'value' => $existingValue]; + $this->optionsText[$existingValue] = $existingLabel; + } + foreach ($this->listAvailableOptions($entity) as $handle) { + $options[] = ['label' => $handle, 'value' => $handle]; + $this->optionsText[$handle] = $handle; + } + + return $options; + } +} diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 4ddfd1f3b63a8..330debdc32469 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -5,13 +5,10 @@ */ namespace Magento\Catalog\Model; -use Magento\Authorization\Model\UserContextInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Framework\Api\AttributeValueFactory; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\AuthorizationInterface; use Magento\Framework\Convert\ConvertArray; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Profiler; @@ -131,7 +128,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements 'page_layout', 'custom_layout_update', 'custom_apply_to_products', - 'custom_use_parent_settings', + 'custom_layout_update_file', + 'custom_use_parent_settings' ]; /** @@ -215,16 +213,6 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements */ protected $metadataService; - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * @var AuthorizationInterface - */ - private $authorization; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -495,7 +483,7 @@ public function getProductCollection() * Retrieve all customer attributes * * @param bool $noDesignAttributes - * @return array + * @return \Magento\Eav\Api\Data\AttributeInterface[] * @todo Use with Flat Resource * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -764,7 +752,7 @@ public function getCustomDesignDate() /** * Retrieve design attributes array * - * @return array + * @return \Magento\Eav\Api\Data\AttributeInterface[] */ public function getDesignAttributes() { @@ -936,60 +924,6 @@ public function beforeDelete() return parent::beforeDelete(); } - /** - * Get user context. - * - * @return UserContextInterface - */ - private function getUserContext(): UserContextInterface - { - if (!$this->userContext) { - $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class); - } - - return $this->userContext; - } - - /** - * Get authorization service. - * - * @return AuthorizationInterface - */ - private function getAuthorization(): AuthorizationInterface - { - if (!$this->authorization) { - $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class); - } - - return $this->authorization; - } - - /** - * @inheritDoc - */ - public function beforeSave() - { - //Validate changing of design. - $userType = $this->getUserContext()->getUserType(); - if (( - $userType === UserContextInterface::USER_TYPE_ADMIN - || $userType === UserContextInterface::USER_TYPE_INTEGRATION - ) - && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_category_design') - ) { - foreach ($this->_designAttributes as $attributeCode) { - $this->setData($attributeCode, $value = $this->getOrigData($attributeCode)); - if (!empty($this->_data[self::CUSTOM_ATTRIBUTES]) - && array_key_exists($attributeCode, $this->_data[self::CUSTOM_ATTRIBUTES])) { - //In case custom attribute were used to update the entity. - $this->_data[self::CUSTOM_ATTRIBUTES][$attributeCode]->setValue($value); - } - } - } - - return parent::beforeSave(); - } - /** * Retrieve anchors above * @@ -1201,8 +1135,6 @@ public function reindex() || $this->dataHasChangedFor('is_active')) { if (!$productIndexer->isScheduled()) { $productIndexer->reindexList($this->getPathIds()); - } else { - $productIndexer->invalidate(); } } } @@ -1360,6 +1292,7 @@ public function getChildrenData() //@codeCoverageIgnoreEnd + // phpcs:disable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames /** * Return Data Object data in array format. * @@ -1368,6 +1301,7 @@ public function getChildrenData() */ public function __toArray() { + // phpcs:enable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames $data = $this->_data; $hasToArray = function ($model) { return is_object($model) && method_exists($model, '__toArray') && is_callable([$model, '__toArray']); diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php new file mode 100644 index 0000000000000..215fe1c19bd8d --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Attribute\Backend; + +use Magento\Catalog\Model\AbstractModel; +use Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; + +/** + * Allows to select a layout file to merge when rendering the category's page. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + * + * @param AbstractModel|Category $forModel + */ + protected function listAvailableValues(AbstractModel $forModel): array + { + return $this->manager->fetchAvailableFiles($forModel); + } +} diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php b/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php new file mode 100644 index 0000000000000..f5694a46d3fb2 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Attribute; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Framework\App\Area; +use Magento\Framework\DataObject; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Model\Layout\Merge as LayoutProcessor; +use Magento\Framework\View\Model\Layout\MergeFactory as LayoutProcessorFactory; + +/** + * Manage available layout updates for categories. + */ +class LayoutUpdateManager +{ + + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var LayoutProcessorFactory + */ + private $layoutProcessorFactory; + + /** + * @var LayoutProcessor|null + */ + private $layoutProcessor; + + /** + * @param FlyweightFactory $themeFactory + * @param DesignInterface $design + * @param LayoutProcessorFactory $layoutProcessorFactory + */ + public function __construct( + FlyweightFactory $themeFactory, + DesignInterface $design, + LayoutProcessorFactory $layoutProcessorFactory + ) { + $this->themeFactory = $themeFactory; + $this->design = $design; + $this->layoutProcessorFactory = $layoutProcessorFactory; + } + + /** + * Get the processor instance. + * + * @return LayoutProcessor + */ + private function getLayoutProcessor(): LayoutProcessor + { + if (!$this->layoutProcessor) { + $this->layoutProcessor = $this->layoutProcessorFactory->create( + [ + 'theme' => $this->themeFactory->create( + $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND) + ) + ] + ); + $this->themeFactory = null; + $this->design = null; + } + + return $this->layoutProcessor; + } + + /** + * Fetch list of available files/handles for the category. + * + * @param CategoryInterface $category + * @return string[] + */ + public function fetchAvailableFiles(CategoryInterface $category): array + { + if (!$category->getId()) { + return []; + } + + $handles = $this->getLayoutProcessor()->getAvailableHandles(); + + return array_filter( + array_map( + function (string $handle) use ($category) : ?string { + preg_match( + '/^catalog\_category\_view\_selectable\_' .$category->getId() .'\_([a-z0-9]+)/i', + $handle, + $selectable + ); + if (!empty($selectable[1])) { + return $selectable[1]; + } + + return null; + }, + $handles + ) + ); + } + + /** + * Extract custom layout attribute value. + * + * @param CategoryInterface $category + * @return mixed + */ + private function extractAttributeValue(CategoryInterface $category) + { + if ($category instanceof Category && !$category->hasData(CategoryInterface::CUSTOM_ATTRIBUTES)) { + return $category->getData('custom_layout_update_file'); + } + if ($attr = $category->getCustomAttribute('custom_layout_update_file')) { + return $attr->getValue(); + } + + return null; + } + + /** + * Extract selected custom layout settings. + * + * If no update is selected none will apply. + * + * @param CategoryInterface $category + * @param DataObject $intoSettings + * @return void + */ + public function extractCustomSettings(CategoryInterface $category, DataObject $intoSettings): void + { + if ($category->getId() && $value = $this->extractAttributeValue($category)) { + $handles = $intoSettings->getPageLayoutHandles() ?? []; + $handles = array_merge_recursive( + $handles, + ['selectable' => $category->getId() . '_' . $value] + ); + $intoSettings->setPageLayoutHandles($handles); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php new file mode 100644 index 0000000000000..1c307220aa9f8 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Attribute\Source; + +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Catalog\Model\Attribute\Source\AbstractLayoutUpdate; + +/** + * List of layout updates available for a category. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + */ + protected function listAvailableOptions(CustomAttributesDataInterface $entity): array + { + return $this->manager->fetchAvailableFiles($entity); + } +} diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php index 97bc00bc7dd64..4dda2fe5786e3 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php @@ -40,7 +40,7 @@ protected function _getCatalogConfig() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllOptions() { @@ -49,7 +49,7 @@ public function getAllOptions() foreach ($this->_getCatalogConfig()->getAttributesUsedForSortBy() as $attribute) { $this->_options[] = [ 'label' => __($attribute['frontend_label']), - 'value' => $attribute['attribute_code'], + 'value' => $attribute['attribute_code'] ]; } } diff --git a/app/code/Magento/Catalog/Model/Category/Authorization.php b/app/code/Magento/Catalog/Model/Category/Authorization.php new file mode 100644 index 0000000000000..629a3c2319472 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Authorization.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; + +/** + * Additional authorization for category operations. + */ +class Authorization +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var CategoryFactory + */ + private $categoryFactory; + + /** + * @param AuthorizationInterface $authorization + * @param CategoryFactory $factory + */ + public function __construct(AuthorizationInterface $authorization, CategoryFactory $factory) + { + $this->authorization = $authorization; + $this->categoryFactory = $factory; + } + + /** + * Extract attribute value from the model. + * + * @param CategoryModel $category + * @param AttributeInterface $attr + * @throws \RuntimeException When no new value is present. + * @return mixed + */ + private function extractAttributeValue(CategoryModel $category, AttributeInterface $attr) + { + if ($category->hasData($attr->getAttributeCode())) { + $newValue = $category->getData($attr->getAttributeCode()); + } elseif ($category->hasData(CategoryModel::CUSTOM_ATTRIBUTES) + && $attrValue = $category->getCustomAttribute($attr->getAttributeCode()) + ) { + $newValue = $attrValue->getValue(); + } else { + throw new \RuntimeException('New value is not set'); + } + + if (empty($newValue) + || ($attr->getBackend() instanceof LayoutUpdate + && ($newValue === LayoutUpdate::VALUE_USE_UPDATE_XML || $newValue === LayoutUpdate::VALUE_NO_UPDATE) + ) + ) { + $newValue = null; + } + + return $newValue; + } + + /** + * Find values to compare the new one. + * + * @param AttributeInterface $attribute + * @param array|null $oldCategory + * @return mixed[] + */ + private function fetchOldValue(AttributeInterface $attribute, ?array $oldCategory): array + { + $oldValues = [null]; + $attrCode = $attribute->getAttributeCode(); + if ($oldCategory) { + //New value must match saved value exactly + $oldValues = [!empty($oldCategory[$attrCode]) ? $oldCategory[$attrCode] : null]; + if (empty($oldValues[0])) { + $oldValues[0] = null; + } + } else { + //New value can be either empty or default value. + $oldValues[] = $attribute->getDefaultValue(); + } + + return $oldValues; + } + + /** + * Determine whether a category has design properties changed. + * + * @param CategoryModel $category + * @param array|null $oldCategory + * @return bool + */ + private function hasChanges(CategoryModel $category, ?array $oldCategory): bool + { + foreach ($category->getDesignAttributes() as $designAttribute) { + $oldValues = $this->fetchOldValue($designAttribute, $oldCategory); + try { + $newValue = $this->extractAttributeValue($category, $designAttribute); + } catch (\RuntimeException $exception) { + //No new value + continue; + } + + if (!in_array($newValue, $oldValues, true)) { + return true; + } + } + + return false; + } + + /** + * Authorize saving of a category. + * + * @throws AuthorizationException + * @throws NoSuchEntityException When a category with invalid ID given. + * @param CategoryInterface|CategoryModel $category + * @return void + */ + public function authorizeSavingOf(CategoryInterface $category): void + { + if (!$this->authorization->isAllowed('Magento_Catalog::edit_category_design')) { + $oldData = null; + if ($category->getId()) { + if ($category->getOrigData()) { + $oldData = $category->getOrigData(); + } else { + /** @var CategoryModel $savedCategory */ + $savedCategory = $this->categoryFactory->create(); + $savedCategory->load($category->getId()); + if (!$savedCategory->getName()) { + throw NoSuchEntityException::singleField('id', $category->getId()); + } + $oldData = $savedCategory->getData(); + } + } + + if ($this->hasChanges($category, $oldData)) { + throw new AuthorizationException(__('Not allowed to edit the category\'s design attributes')); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index c96b2aae36059..283e3f87686b9 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Category; use Magento\Catalog\Api\Data\CategoryInterface; @@ -15,11 +17,13 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface; use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\Stdlib\ArrayUtils; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Form\Field; @@ -28,10 +32,9 @@ use Magento\Framework\AuthorizationInterface; /** - * Class DataProvider + * Category form data provider. * * @api - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @since 101.0.0 @@ -52,6 +55,7 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider /** * EAV attribute properties to fetch from meta storage + * * @var array * @since 101.0.0 */ @@ -66,6 +70,8 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider 'size' => 'multiline_count', ]; + private $boolMetaProperties = ['visible', 'required']; + /** * Form element mapping * @@ -143,6 +149,11 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider */ private $arrayManager; + /** + * @var ArrayUtils + */ + private $arrayUtils; + /** * @var Filesystem */ @@ -154,8 +165,6 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider private $auth; /** - * DataProvider constructor - * * @param string $name * @param string $primaryFieldName * @param string $requestFieldName @@ -170,6 +179,8 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider * @param array $data * @param PoolInterface|null $pool * @param AuthorizationInterface|null $auth + * @param ArrayUtils|null $arrayUtils + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -186,7 +197,8 @@ public function __construct( array $meta = [], array $data = [], PoolInterface $pool = null, - ?AuthorizationInterface $auth = null + ?AuthorizationInterface $auth = null, + ?ArrayUtils $arrayUtils = null ) { $this->eavValidationRules = $eavValidationRules; $this->collection = $categoryCollectionFactory->create(); @@ -197,6 +209,7 @@ public function __construct( $this->request = $request; $this->categoryFactory = $categoryFactory; $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); + $this->arrayUtils = $arrayUtils ?? ObjectManager::getInstance()->get(ArrayUtils::class); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); } @@ -226,7 +239,7 @@ public function getMeta() * @param array $meta * @return array */ - private function addUseDefaultValueCheckbox(Category $category, array $meta) + private function addUseDefaultValueCheckbox(Category $category, array $meta): array { /** @var EavAttributeInterface $attribute */ foreach ($category->getAttributes() as $attribute) { @@ -290,7 +303,7 @@ public function prepareMeta($meta) * @param array $fieldsMeta * @return array */ - private function prepareFieldsMeta($fieldsMap, $fieldsMeta) + private function prepareFieldsMeta(array $fieldsMap, array $fieldsMeta): array { $canEditDesign = $this->auth->isAllowed('Magento_Catalog::edit_category_design'); @@ -350,6 +363,8 @@ public function getAttributesMeta(Type $entityType) { $meta = []; $attributes = $entityType->getAttributeCollection(); + $fields = $this->getFields(); + $category = $this->getCurrentCategory(); /* @var EavAttribute $attribute */ foreach ($attributes as $attribute) { $code = $attribute->getAttributeCode(); @@ -357,13 +372,26 @@ public function getAttributesMeta(Type $entityType) foreach ($this->metaProperties as $metaName => $origName) { $value = $attribute->getDataUsingMethod($origName); $meta[$code][$metaName] = $value; + if (in_array($metaName, $this->boolMetaProperties, true)) { + $meta[$code][$metaName] = (bool)$meta[$code][$metaName]; + } if ('frontend_input' === $origName) { $meta[$code]['formElement'] = isset($this->formElement[$value]) ? $this->formElement[$value] : $value; } if ($attribute->usesSource()) { - $meta[$code]['options'] = $attribute->getSource()->getAllOptions(); + $source = $attribute->getSource(); + $currentCategory = $this->getCurrentCategory(); + if ($source instanceof SpecificSourceInterface && $currentCategory) { + $options = $source->getOptionsFor($currentCategory); + } else { + $options = $source->getAllOptions(); + } + foreach ($options as &$option) { + $option['__disableTmpl'] = true; + } + $meta[$code]['options'] = $options; } } @@ -374,6 +402,16 @@ public function getAttributesMeta(Type $entityType) $meta[$code]['scopeLabel'] = $this->getScopeLabel($attribute); $meta[$code]['componentType'] = Field::NAME; + + // disable fields + if ($category) { + $attributeIsLocked = $category->isLockedAttribute($code); + $meta[$code]['disabled'] = $attributeIsLocked; + $hasUseConfigField = (bool) array_search('use_config.' . $code, $fields, true); + if ($hasUseConfigField && $meta[$code]['disabled']) { + $meta['use_config.' . $code]['disabled'] = true; + } + } } $result = []; @@ -505,9 +543,15 @@ protected function filterFields($categoryData) * @param array $categoryData * @return array */ - private function convertValues($category, $categoryData) + private function convertValues($category, $categoryData): array { foreach ($category->getAttributes() as $attributeCode => $attribute) { + if ($attributeCode === 'custom_layout_update_file') { + if (!empty($categoryData['custom_layout_update'])) { + $categoryData['custom_layout_update_file'] + = \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::VALUE_USE_UPDATE_XML; + } + } if (!isset($categoryData[$attributeCode])) { continue; } @@ -604,6 +648,7 @@ protected function getFieldsMap() 'custom_design', 'page_layout', 'custom_layout_update', + 'custom_layout_update_file' ], 'schedule_design_update' => [ 'custom_design_from', @@ -616,13 +661,24 @@ protected function getFieldsMap() ]; } + /** + * Return list of fields names. + * + * @return array + */ + private function getFields(): array + { + $fieldsMap = $this->getFieldsMap(); + return $this->arrayUtils->flatten($fieldsMap); + } + /** * Retrieve scope overridden value * * @return ScopeOverriddenValue * @deprecated 101.1.0 */ - private function getScopeOverriddenValue() + private function getScopeOverriddenValue(): ScopeOverriddenValue { if (null === $this->scopeOverriddenValue) { $this->scopeOverriddenValue = \Magento\Framework\App\ObjectManager::getInstance()->get( @@ -639,7 +695,7 @@ private function getScopeOverriddenValue() * @return ArrayManager * @deprecated 101.1.0 */ - private function getArrayManager() + private function getArrayManager(): ArrayManager { if (null === $this->arrayManager) { $this->arrayManager = \Magento\Framework\App\ObjectManager::getInstance()->get( @@ -657,7 +713,7 @@ private function getArrayManager() * * @deprecated 101.1.0 */ - private function getFileInfo() + private function getFileInfo(): FileInfo { if ($this->fileInfo === null) { $this->fileInfo = ObjectManager::getInstance()->get(FileInfo::class); diff --git a/app/code/Magento/Catalog/Model/CategoryAttributeSearchResults.php b/app/code/Magento/Catalog/Model/CategoryAttributeSearchResults.php new file mode 100644 index 0000000000000..db1b84ed27772 --- /dev/null +++ b/app/code/Magento/Catalog/Model/CategoryAttributeSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Category Attribute search results. + */ +class CategoryAttributeSearchResults extends SearchResults implements CategoryAttributeSearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/CategorySearchResults.php b/app/code/Magento/Catalog/Model/CategorySearchResults.php new file mode 100644 index 0000000000000..7590ee4a23eda --- /dev/null +++ b/app/code/Magento/Catalog/Model/CategorySearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\CategorySearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Category search results. + */ +class CategorySearchResults extends SearchResults implements CategorySearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php index 5dce940308a4f..c4ff12bbf0f94 100644 --- a/app/code/Magento/Catalog/Model/Config.php +++ b/app/code/Magento/Catalog/Model/Config.php @@ -9,6 +9,8 @@ use Magento\Framework\Serialize\SerializerInterface; /** + * Catalog config model. + * * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -133,6 +135,7 @@ class Config extends \Magento\Eav\Model\Config * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Eav\Model\Config $eavConfig * @param SerializerInterface $serializer + * @param array $attributesForPreload * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -149,7 +152,8 @@ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setCollectionFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Eav\Model\Config $eavConfig, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + $attributesForPreload = [] ) { $this->_scopeConfig = $scopeConfig; $this->_configFactory = $configFactory; @@ -165,7 +169,9 @@ public function __construct( $entityTypeCollectionFactory, $cacheState, $universalFactory, - $serializer + $serializer, + $scopeConfig, + $attributesForPreload ); } diff --git a/app/code/Magento/Catalog/Model/Design.php b/app/code/Magento/Catalog/Model/Design.php index 853bbeac8eb38..fed18a5a60913 100644 --- a/app/code/Magento/Catalog/Model/Design.php +++ b/app/code/Magento/Catalog/Model/Design.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager as CategoryLayoutManager; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager as ProductLayoutManager; +use Magento\Framework\App\ObjectManager; use \Magento\Framework\TranslateInterface; /** @@ -14,6 +17,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Design extends \Magento\Framework\Model\AbstractModel { @@ -38,6 +42,16 @@ class Design extends \Magento\Framework\Model\AbstractModel */ private $translator; + /** + * @var CategoryLayoutManager + */ + private $categoryLayoutUpdates; + + /** + * @var ProductLayoutManager + */ + private $productLayoutUpdates; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -47,6 +61,9 @@ class Design extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param TranslateInterface|null $translator + * @param CategoryLayoutManager|null $categoryLayoutManager + * @param ProductLayoutManager|null $productLayoutManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -56,12 +73,17 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - TranslateInterface $translator = null + TranslateInterface $translator = null, + ?CategoryLayoutManager $categoryLayoutManager = null, + ?ProductLayoutManager $productLayoutManager = null ) { $this->_localeDate = $localeDate; $this->_design = $design; - $this->translator = $translator ?: - \Magento\Framework\App\ObjectManager::getInstance()->get(TranslateInterface::class); + $this->translator = $translator ?? ObjectManager::getInstance()->get(TranslateInterface::class); + $this->categoryLayoutUpdates = $categoryLayoutManager + ?? ObjectManager::getInstance()->get(CategoryLayoutManager::class); + $this->productLayoutUpdates = $productLayoutManager + ?? ObjectManager::getInstance()->get(ProductLayoutManager::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -81,12 +103,12 @@ public function applyCustomDesign($design) /** * Get custom layout settings * - * @param \Magento\Catalog\Model\Category|\Magento\Catalog\Model\Product $object + * @param Category|Product $object * @return \Magento\Framework\DataObject */ public function getDesignSettings($object) { - if ($object instanceof \Magento\Catalog\Model\Product) { + if ($object instanceof Product) { $currentCategory = $object->getCategory(); } else { $currentCategory = $object; @@ -97,7 +119,7 @@ public function getDesignSettings($object) $category = $currentCategory->getParentDesignCategory($currentCategory); } - if ($object instanceof \Magento\Catalog\Model\Product) { + if ($object instanceof Product) { if ($category && $category->getCustomApplyToProducts()) { return $this->_mergeSettings($this->_extractSettings($category), $this->_extractSettings($object)); } else { @@ -111,7 +133,7 @@ public function getDesignSettings($object) /** * Extract custom layout settings from category or product object * - * @param \Magento\Catalog\Model\Category|\Magento\Catalog\Model\Product $object + * @param Category|Product $object * @return \Magento\Framework\DataObject */ protected function _extractSettings($object) @@ -140,6 +162,11 @@ protected function _extractSettings($object) )->setLayoutUpdates( (array)$object->getCustomLayoutUpdate() ); + if ($object instanceof Category) { + $this->categoryLayoutUpdates->extractCustomSettings($object, $settings); + } elseif ($object instanceof Product) { + $this->productLayoutUpdates->extractCustomSettings($object, $settings); + } } return $settings; } diff --git a/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php b/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php index 22cb3c3264df5..d9893be3125fe 100644 --- a/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php +++ b/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php @@ -21,7 +21,7 @@ class Container extends \Magento\Eav\Model\Entity\Attribute\Source\Config public function getOptionText($value) { $options = $this->getAllOptions(); - if (sizeof($options) > 0) { + if (count($options) > 0) { foreach ($options as $option) { if (isset($option['value']) && $option['value'] == $value) { return __($option['label']); diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product.php index af1cda41d8c46..c18404bda1fc8 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product.php @@ -137,10 +137,11 @@ protected function executeAction($ids) /** @var Product\Action\Rows $action */ $action = $this->rowsActionFactory->create(); - if ($indexer->isWorking()) { + if ($indexer->isScheduled()) { $action->execute($ids, true); + } else { + $action->execute($ids); } - $action->execute($ids); return $this; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index 3bd4910767587..5d81c1405efe0 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -5,6 +5,25 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Product\Action; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Category; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; + +/** + * Reindex multiple rows action. + * + * @package Magento\Catalog\Model\Indexer\Category\Product\Action + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction { /** @@ -14,25 +33,121 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ protected $limitationByCategories; + /** + * @var CacheContext + */ + private $cacheContext; + + /** + * @var EventManagerInterface|null + */ + private $eventManager; + + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @param ResourceConnection $resource + * @param StoreManagerInterface $storeManager + * @param Config $config + * @param QueryGenerator|null $queryGenerator + * @param MetadataPool|null $metadataPool + * @param CacheContext|null $cacheContext + * @param EventManagerInterface|null $eventManager + * @param IndexerRegistry|null $indexerRegistry + */ + public function __construct( + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, + QueryGenerator $queryGenerator = null, + MetadataPool $metadataPool = null, + CacheContext $cacheContext = null, + EventManagerInterface $eventManager = null, + IndexerRegistry $indexerRegistry = null + ) { + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); + $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); + } + /** * Refresh entities index * * @param int[] $entityIds * @param bool $useTempTable * @return $this + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute(array $entityIds = [], $useTempTable = false) { - $this->limitationByCategories = $entityIds; + foreach ($entityIds as $entityId) { + $this->limitationByCategories[] = (int)$entityId; + $path = $this->getPathFromCategoryId($entityId); + if (!empty($path)) { + $pathIds = explode('/', $path); + foreach ($pathIds as $pathId) { + $this->limitationByCategories[] = (int)$pathId; + } + } + } + $this->limitationByCategories = array_unique($this->limitationByCategories); $this->useTempTable = $useTempTable; + $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); + $workingState = $indexer->isWorking(); - $this->removeEntries(); + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->getIndexTable($store->getId())); + } + } else { + $this->removeEntries(); + } $this->reindex(); + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]); + $this->connection->delete( + $this->tableMaintainer->getMainTable($store->getId()), + ['category_id IN (?)' => $removalCategoryIds] + ); + $select = $this->connection->select() + ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->query( + $this->connection->insertFromSelect( + $select, + $this->tableMaintainer->getMainTable($store->getId()), + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } + } + + $this->registerCategories($entityIds); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + return $this; } + /** + * Register categories assigned to products + * + * @param array $categoryIds + * @return void + */ + private function registerCategories(array $categoryIds) + { + if ($categoryIds) { + $this->cacheContext->registerEntities(Category::CACHE_TAG, $categoryIds); + } + } + /** * Return array of all category root IDs + tree root ID * diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index cb708695255d4..ec3d0d57330ec 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -15,6 +15,9 @@ use Magento\Framework\Event\ManagerInterface as EventManagerInterface; use Magento\Framework\Indexer\CacheContext; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; /** * Category rows indexer. @@ -40,6 +43,11 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private $eventManager; + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + /** * @param ResourceConnection $resource * @param StoreManagerInterface $storeManager @@ -48,6 +56,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio * @param MetadataPool|null $metadataPool * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager + * @param IndexerRegistry|null $indexerRegistry */ public function __construct( ResourceConnection $resource, @@ -56,11 +65,13 @@ public function __construct( QueryGenerator $queryGenerator = null, MetadataPool $metadataPool = null, CacheContext $cacheContext = null, - EventManagerInterface $eventManager = null + EventManagerInterface $eventManager = null, + IndexerRegistry $indexerRegistry = null ) { parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); + $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); } /** @@ -78,12 +89,37 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; + $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); + $workingState = $indexer->isWorking(); $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); - $this->removeEntries(); - + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->getIndexTable($store->getId())); + } + } else { + $this->removeEntries(); + } $this->reindex(); + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->delete( + $this->tableMaintainer->getMainTable($store->getId()), + ['product_id IN (?)' => $this->limitationByProducts] + ); + $select = $this->connection->select() + ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->query( + $this->connection->insertFromSelect( + $select, + $this->tableMaintainer->getMainTable($store->getId()), + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } + } $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); @@ -105,7 +141,7 @@ public function execute(array $entityIds = [], $useTempTable = false) * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface * @throws \DomainException */ - private function getProductIdsWithParents(array $childProductIds) + private function getProductIdsWithParents(array $childProductIds): array { /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); @@ -123,8 +159,12 @@ private function getProductIdsWithParents(array $childProductIds) ); $parentProductIds = $this->connection->fetchCol($select); + $ids = array_unique(array_merge($childProductIds, $parentProductIds)); + foreach ($ids as $key => $id) { + $ids[$key] = (int) $id; + } - return array_unique(array_merge($childProductIds, $parentProductIds)); + return $ids; } /** @@ -175,7 +215,7 @@ protected function removeEntries() protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) { $select = parent::getNonAnchorCategoriesSelect($store); - return $select->where('ccp.product_id IN (?) OR relation.child_id IN (?)', $this->limitationByProducts); + return $select->where('ccp.product_id IN (?)', $this->limitationByProducts); } /** @@ -216,28 +256,28 @@ protected function isRangingNeeded() * Returns a list of category ids which are assigned to product ids in the index * * @param array $productIds - * @return \Magento\Framework\Indexer\CacheContext + * @return array */ - private function getCategoryIdsFromIndex(array $productIds) + private function getCategoryIdsFromIndex(array $productIds): array { $categoryIds = []; foreach ($this->storeManager->getStores() as $store) { - $categoryIds = array_merge( - $categoryIds, - $this->connection->fetchCol( - $this->connection->select() - ->from($this->getIndexTable($store->getId()), ['category_id']) - ->where('product_id IN (?)', $productIds) - ->distinct() - ) + $storeCategories = $this->connection->fetchCol( + $this->connection->select() + ->from($this->getIndexTable($store->getId()), ['category_id']) + ->where('product_id IN (?)', $productIds) + ->distinct() ); + $categoryIds[] = $storeCategories; } - $parentCategories = $categoryIds; + $categoryIds = array_merge(...$categoryIds); + + $parentCategories = [$categoryIds]; foreach ($categoryIds as $categoryId) { $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); - $parentCategories = array_merge($parentCategories, $parentIds); + $parentCategories[] = $parentIds; } - $categoryIds = array_unique($parentCategories); + $categoryIds = array_unique(array_merge(...$parentCategories)); return $categoryIds; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php index ebad10e197622..a0acacd4dfd2f 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/AbstractAction.php @@ -254,10 +254,12 @@ protected function _updateRelationProducts($storeId, $productIds = null) * * @param int $storeId * @return \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _cleanRelationProducts($storeId) { - if (!$this->_productIndexerHelper->isAddChildData()) { + if (!$this->_productIndexerHelper->isAddChildData() || !$this->_isFlatTableExists($storeId)) { return $this; } diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php index d1aee8c4c5ba6..229844fbe84b5 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php @@ -10,6 +10,9 @@ use Magento\Framework\Registry; use Magento\Store\Model\ScopeInterface; +/** + * Data provider for price filter in layered navigation + */ class Price { /** @@ -103,6 +106,8 @@ public function __construct( } /** + * Getter for interval + * * @return array */ public function getInterval() @@ -111,6 +116,8 @@ public function getInterval() } /** + * Setter for interval + * * @param array $interval * @return void */ @@ -120,6 +127,10 @@ public function setInterval($interval) } /** + * Retrieves price layered navigation modes + * + * @see RANGE_CALCULATION_AUTO + * * @return mixed */ public function getRangeCalculationValue() @@ -131,6 +142,8 @@ public function getRangeCalculationValue() } /** + * Retrieves range step + * * @return mixed */ public function getRangeStepValue() @@ -142,6 +155,8 @@ public function getRangeStepValue() } /** + * Retrieves one price interval + * * @return mixed */ public function getOnePriceIntervalValue() @@ -179,6 +194,8 @@ public function getRangeMaxIntervalsValue() } /** + * Retrieves Catalog Layer object + * * @return Layer */ public function getLayer() @@ -276,6 +293,8 @@ public function getMaxPrice() } /** + * Retrieve list of prior filters + * * @param string $filterParams * @return array */ @@ -310,7 +329,7 @@ public function validateFilter($filter) return false; } foreach ($filter as $v) { - if ($v !== '' && $v !== '0' && (double)$v <= 0 || is_infinite((double)$v)) { + if ($v !== '' && $v !== '0' && (!is_numeric($v) || (double)$v <= 0 || is_infinite((double)$v))) { return false; } } @@ -339,6 +358,8 @@ public function getResetValue() } /** + * Getter for prior intervals + * * @return array */ public function getPriorIntervals() @@ -347,6 +368,8 @@ public function getPriorIntervals() } /** + * Setter for prior intervals + * * @param array $priorInterval * @return void */ @@ -356,6 +379,8 @@ public function setPriorIntervals($priorInterval) } /** + * Get Resource model for price filter + * * @return \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price */ public function getResource() @@ -364,6 +389,8 @@ public function getResource() } /** + * Retrieves additional request data + * * @return string */ public function getAdditionalRequestData() diff --git a/app/code/Magento/Catalog/Model/Layer/FilterList.php b/app/code/Magento/Catalog/Model/Layer/FilterList.php index 9d7b71c981c6b..b8e9b8ad4aaa5 100644 --- a/app/code/Magento/Catalog/Model/Layer/FilterList.php +++ b/app/code/Magento/Catalog/Model/Layer/FilterList.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Layer; +/** + * Layer navigation filters + */ class FilterList { const CATEGORY_FILTER = 'category'; @@ -106,9 +110,9 @@ protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\ { $filterClassName = $this->filterTypes[self::ATTRIBUTE_FILTER]; - if ($attribute->getAttributeCode() == 'price') { + if ($attribute->getFrontendInput() === 'price') { $filterClassName = $this->filterTypes[self::PRICE_FILTER]; - } elseif ($attribute->getBackendType() == 'decimal') { + } elseif ($attribute->getBackendType() === 'decimal') { $filterClassName = $this->filterTypes[self::DECIMAL_FILTER]; } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index fc9fffb2a7e9a..7015fa0295cfb 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -5,7 +5,6 @@ */ namespace Magento\Catalog\Model; -use Magento\Authorization\Model\UserContextInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterface; @@ -15,7 +14,6 @@ use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -use Magento\Framework\AuthorizationInterface; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Pricing\SaleableInterface; @@ -177,7 +175,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements protected $_catalogProduct = null; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -355,16 +353,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements */ private $filterCustomAttribute; - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * @var AuthorizationInterface - */ - private $authorization; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -381,7 +369,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements * @param Product\Attribute\Source\Status $catalogProductStatus * @param Product\Media\Config $catalogProductMediaConfig * @param Product\Type $catalogProductType - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Helper\Product $catalogProduct * @param ResourceModel\Product $resource * @param ResourceModel\Product\Collection $resourceCollection @@ -422,7 +410,7 @@ public function __construct( \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, \Magento\Catalog\Model\Product\Media\Config $catalogProductMediaConfig, Product\Type $catalogProductType, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Helper\Product $catalogProduct, \Magento\Catalog\Model\ResourceModel\Product $resource, \Magento\Catalog\Model\ResourceModel\Product\Collection $resourceCollection, @@ -832,14 +820,17 @@ public function getStoreIds() if (!$this->hasStoreIds()) { $storeIds = []; if ($websiteIds = $this->getWebsiteIds()) { - if ($this->_storeManager->isSingleStoreMode()) { + if (!$this->isObjectNew() && $this->_storeManager->isSingleStoreMode()) { $websiteIds = array_keys($websiteIds); } foreach ($websiteIds as $websiteId) { $websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds(); - $storeIds = array_merge($storeIds, $websiteStores); + $storeIds[] = $websiteStores; } } + if ($storeIds) { + $storeIds = array_merge(...$storeIds); + } $this->setStoreIds($storeIds); } return $this->getData('store_ids'); @@ -872,34 +863,6 @@ public function getAttributes($groupId = null, $skipSuper = false) return $attributes; } - /** - * Get user context. - * - * @return UserContextInterface - */ - private function getUserContext(): UserContextInterface - { - if (!$this->userContext) { - $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class); - } - - return $this->userContext; - } - - /** - * Get authorization service. - * - * @return AuthorizationInterface - */ - private function getAuthorization(): AuthorizationInterface - { - if (!$this->authorization) { - $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class); - } - - return $this->authorization; - } - /** * Check product options and type options and save them, too * @@ -917,22 +880,6 @@ public function beforeSave() $this->getTypeInstance()->beforeSave($this); - //Validate changing of design. - $userType = $this->getUserContext()->getUserType(); - if (( - $userType === UserContextInterface::USER_TYPE_ADMIN - || $userType === UserContextInterface::USER_TYPE_INTEGRATION - ) - && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_product_design') - ) { - $this->setData('custom_design', $this->getOrigData('custom_design')); - $this->setData('page_layout', $this->getOrigData('page_layout')); - $this->setData('options_container', $this->getOrigData('options_container')); - $this->setData('custom_layout_update', $this->getOrigData('custom_layout_update')); - $this->setData('custom_design_from', $this->getOrigData('custom_design_from')); - $this->setData('custom_design_to', $this->getOrigData('custom_design_to')); - } - $hasOptions = false; $hasRequiredOptions = false; @@ -1324,12 +1271,11 @@ public function getSpecialToDate() public function getRelatedProducts() { if (!$this->hasRelatedProducts()) { - $products = []; - $collection = $this->getRelatedProductCollection(); - foreach ($collection as $product) { - $products[] = $product; + //Loading all linked products. + $this->getProductLinks(); + if (!$this->hasRelatedProducts()) { + $this->setRelatedProducts([]); } - $this->setRelatedProducts($products); } return $this->getData('related_products'); } @@ -1386,12 +1332,13 @@ public function getRelatedLinkCollection() public function getUpSellProducts() { if (!$this->hasUpSellProducts()) { - $products = []; - foreach ($this->getUpSellProductCollection() as $product) { - $products[] = $product; + //Loading all linked products. + $this->getProductLinks(); + if (!$this->hasUpSellProducts()) { + $this->setUpSellProducts([]); } - $this->setUpSellProducts($products); } + return $this->getData('up_sell_products'); } @@ -1447,12 +1394,13 @@ public function getUpSellLinkCollection() public function getCrossSellProducts() { if (!$this->hasCrossSellProducts()) { - $products = []; - foreach ($this->getCrossSellProductCollection() as $product) { - $products[] = $product; + //Loading all linked products. + $this->getProductLinks(); + if (!$this->hasCrossSellProducts()) { + $this->setCrossSellProducts([]); } - $this->setCrossSellProducts($products); } + return $this->getData('cross_sell_products'); } @@ -1508,7 +1456,11 @@ public function getCrossSellLinkCollection() public function getProductLinks() { if ($this->_links === null) { - $this->_links = $this->getLinkRepository()->getList($this); + if ($this->getSku() && $this->getId()) { + $this->_links = $this->getLinkRepository()->getList($this); + } else { + $this->_links = []; + } } return $this->_links; } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php new file mode 100644 index 0000000000000..fa5a218824eea --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Backend; + +use Magento\Catalog\Model\AbstractModel; +use Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager; + +/** + * Allows to select a layout file to merge when rendering the product's page. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + * + * @param AbstractModel|Product $forModel + */ + protected function listAvailableValues(AbstractModel $forModel): array + { + return $this->manager->fetchAvailableFiles($forModel); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php index f1943bc108878..0daa1dfb5c8eb 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php @@ -96,12 +96,7 @@ public function execute($entity, $arguments = []) $productId = (int)$entity->getData($identifierField); // prepare original data to compare - $origPrices = []; - $originalId = $entity->getOrigData($identifierField); - if (empty($originalId) || $entity->getData($identifierField) == $originalId) { - $origPrices = $entity->getOrigData($attribute->getName()); - } - + $origPrices = $entity->getOrigData($attribute->getName()); $old = $this->prepareOldTierPriceToCompare($origPrices); // prepare data for save $new = $this->prepareNewDataForSave($priceRows, $isGlobal); diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php b/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php new file mode 100644 index 0000000000000..92ae989500076 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Area; +use Magento\Framework\DataObject; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Model\Layout\Merge as LayoutProcessor; +use Magento\Framework\View\Model\Layout\MergeFactory as LayoutProcessorFactory; + +/** + * Manage available layout updates for products. + */ +class LayoutUpdateManager +{ + + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var LayoutProcessorFactory + */ + private $layoutProcessorFactory; + + /** + * @var LayoutProcessor|null + */ + private $layoutProcessor; + + /** + * @param FlyweightFactory $themeFactory + * @param DesignInterface $design + * @param LayoutProcessorFactory $layoutProcessorFactory + */ + public function __construct( + FlyweightFactory $themeFactory, + DesignInterface $design, + LayoutProcessorFactory $layoutProcessorFactory + ) { + $this->themeFactory = $themeFactory; + $this->design = $design; + $this->layoutProcessorFactory = $layoutProcessorFactory; + } + + /** + * Adopt product's SKU to be used as layout handle. + * + * @param ProductInterface $product + * @return string + */ + private function sanitizeSku(ProductInterface $product): string + { + return rawurlencode($product->getSku()); + } + + /** + * Get the processor instance. + * + * @return LayoutProcessor + */ + private function getLayoutProcessor(): LayoutProcessor + { + if (!$this->layoutProcessor) { + $this->layoutProcessor = $this->layoutProcessorFactory->create( + [ + 'theme' => $this->themeFactory->create( + $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND) + ) + ] + ); + $this->themeFactory = null; + $this->design = null; + } + + return $this->layoutProcessor; + } + + /** + * Fetch list of available files/handles for the product. + * + * @param ProductInterface $product + * @return string[] + */ + public function fetchAvailableFiles(ProductInterface $product): array + { + if (!$product->getSku()) { + return []; + } + + $identifier = $this->sanitizeSku($product); + $handles = $this->getLayoutProcessor()->getAvailableHandles(); + + return array_filter( + array_map( + function (string $handle) use ($identifier) : ?string { + preg_match( + '/^catalog\_product\_view\_selectable\_' .preg_quote($identifier) .'\_([a-z0-9]+)/i', + $handle, + $selectable + ); + if (!empty($selectable[1])) { + return $selectable[1]; + } + + return null; + }, + $handles + ) + ); + } + + /** + * Extract custom layout attribute value. + * + * @param ProductInterface $product + * @return mixed + */ + private function extractAttributeValue(ProductInterface $product) + { + if ($product instanceof Product && !$product->hasData(ProductInterface::CUSTOM_ATTRIBUTES)) { + return $product->getData('custom_layout_update_file'); + } + if ($attr = $product->getCustomAttribute('custom_layout_update_file')) { + return $attr->getValue(); + } + + return null; + } + + /** + * Extract selected custom layout settings. + * + * If no update is selected none will apply. + * + * @param ProductInterface $product + * @param DataObject $intoSettings + * @return void + */ + public function extractCustomSettings(ProductInterface $product, DataObject $intoSettings): void + { + if ($product->getSku() && $value = $this->extractAttributeValue($product)) { + $handles = $intoSettings->getPageLayoutHandles() ?? []; + $handles = array_merge_recursive( + $handles, + ['selectable' => $this->sanitizeSku($product) . '_' . $value] + ); + $intoSettings->setPageLayoutHandles($handles); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php new file mode 100644 index 0000000000000..0ddb528e768cc --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Source; + +use Magento\Catalog\Model\Attribute\Source\AbstractLayoutUpdate; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager; +use Magento\Framework\Api\CustomAttributesDataInterface; + +/** + * List of layout updates available for a product. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + */ + protected function listAvailableOptions(CustomAttributesDataInterface $entity): array + { + return $this->manager->fetchAvailableFiles($entity); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php b/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php index d0c7103851499..57d08916bcd40 100644 --- a/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php +++ b/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php @@ -5,10 +5,13 @@ */ namespace Magento\Catalog\Model\Product\AttributeSet; +/** + * Attribute Set Options + */ class Options implements \Magento\Framework\Data\OptionSourceInterface { /** - * @var null|array + * @var array */ protected $options; @@ -25,7 +28,7 @@ public function __construct( } /** - * @return array|null + * @inheritDoc */ public function toOptionArray() { @@ -33,7 +36,15 @@ public function toOptionArray() $this->options = $this->collectionFactory->create() ->setEntityTypeFilter($this->product->getTypeId()) ->toOptionArray(); + + array_walk( + $this->options, + function (&$option) { + $option['__disableTmpl'] = true; + } + ); } + return $this->options; } } diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php new file mode 100644 index 0000000000000..b8aa8f70ba70f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Authorization.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product as ProductModel; +use Magento\Catalog\Model\ProductFactory; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate; + +/** + * Additional authorization for product operations. + */ +class Authorization +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var ProductFactory + */ + private $productFactory; + + /** + * @param AuthorizationInterface $authorization + * @param ProductFactory $factory + */ + public function __construct(AuthorizationInterface $authorization, ProductFactory $factory) + { + $this->authorization = $authorization; + $this->productFactory = $factory; + } + + /** + * Extract attribute value from the model. + * + * @param ProductModel $product + * @param AttributeInterface $attr + * @return mixed + * @throws \RuntimeException When no new value is present. + */ + private function extractAttributeValue(ProductModel $product, AttributeInterface $attr) + { + if ($product->hasData($attr->getAttributeCode())) { + $newValue = $product->getData($attr->getAttributeCode()); + } elseif ($product->hasData(ProductModel::CUSTOM_ATTRIBUTES) + && $attrValue = $product->getCustomAttribute($attr->getAttributeCode()) + ) { + $newValue = $attrValue->getValue(); + } else { + throw new \RuntimeException('No new value is present'); + } + + if (empty($newValue) + || ($attr->getBackend() instanceof LayoutUpdate + && ($newValue === LayoutUpdate::VALUE_USE_UPDATE_XML || $newValue === LayoutUpdate::VALUE_NO_UPDATE) + ) + ) { + $newValue = null; + } + + return $newValue; + } + + /** + * Prepare old values to compare to. + * + * @param AttributeInterface $attribute + * @param array|null $oldProduct + * @return array + */ + private function fetchOldValues(AttributeInterface $attribute, ?array $oldProduct): array + { + $attrCode = $attribute->getAttributeCode(); + if ($oldProduct) { + //New value may only be the saved value + $oldValues = [!empty($oldProduct[$attrCode]) ? $oldProduct[$attrCode] : null]; + if (empty($oldValues[0])) { + $oldValues[0] = null; + } + } else { + //New value can be empty or default + $oldValues[] = $attribute->getDefaultValue(); + } + + return $oldValues; + } + + /** + * Check whether the product has changed. + * + * @param ProductModel $product + * @param array|null $oldProduct + * @return bool + */ + private function hasProductChanged(ProductModel $product, ?array $oldProduct = null): bool + { + $designAttributes = [ + 'custom_design', + 'page_layout', + 'options_container', + 'custom_layout_update', + 'custom_design_from', + 'custom_design_to', + 'custom_layout_update_file' + ]; + $attributes = $product->getAttributes(); + + foreach ($designAttributes as $designAttribute) { + if (!array_key_exists($designAttribute, $attributes)) { + continue; + } + $attribute = $attributes[$designAttribute]; + $oldValues = $this->fetchOldValues($attribute, $oldProduct); + try { + $newValue = $this->extractAttributeValue($product, $attribute); + } catch (\RuntimeException $exception) { + //No new value + continue; + } + if (!in_array($newValue, $oldValues, true)) { + return true; + } + } + + return false; + } + + /** + * Authorize saving of a product. + * + * @throws AuthorizationException + * @throws NoSuchEntityException When product with invalid ID given. + * @param ProductInterface|ProductModel $product + * @return void + */ + public function authorizeSavingOf(ProductInterface $product): void + { + if (!$this->authorization->isAllowed('Magento_Catalog::edit_product_design')) { + $oldData = null; + if ($product->getId()) { + if ($product->getOrigData()) { + $oldData = $product->getOrigData(); + } else { + /** @var ProductModel $savedProduct */ + $savedProduct = $this->productFactory->create(); + $savedProduct->load($product->getId()); + if (!$savedProduct->getSku()) { + throw NoSuchEntityException::singleField('id', $product->getId()); + } + $oldData = $product->getOrigData(); + } + } + if ($this->hasProductChanged($product, $oldData)) { + throw new AuthorizationException(__('Not allowed to edit the product\'s design attributes')); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php index 12dbaf0c29f4e..9ccb86441812c 100644 --- a/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php @@ -5,13 +5,17 @@ */ namespace Magento\Catalog\Model\Product\Compare; +use Magento\Catalog\Model\ProductRepository; use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; /** * Product Compare List Model * * @api * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class ListCompare extends \Magento\Framework\DataObject @@ -51,6 +55,11 @@ class ListCompare extends \Magento\Framework\DataObject */ protected $_compareItemFactory; + /** + * @var ProductRepository + */ + private $productRepository; + /** * Constructor * @@ -60,6 +69,7 @@ class ListCompare extends \Magento\Framework\DataObject * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Visitor $customerVisitor * @param array $data + * @param ProductRepository|null $productRepository */ public function __construct( \Magento\Catalog\Model\Product\Compare\ItemFactory $compareItemFactory, @@ -67,13 +77,15 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem, \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Visitor $customerVisitor, - array $data = [] + array $data = [], + ProductRepository $productRepository = null ) { $this->_compareItemFactory = $compareItemFactory; $this->_itemCollectionFactory = $itemCollectionFactory; $this->_catalogProductCompareItem = $catalogProductCompareItem; $this->_customerSession = $customerSession; $this->_customerVisitor = $customerVisitor; + $this->productRepository = $productRepository ?: ObjectManager::getInstance()->create(ProductRepository::class); parent::__construct($data); } @@ -82,6 +94,7 @@ public function __construct( * * @param int|\Magento\Catalog\Model\Product $product * @return $this + * @throws \Exception */ public function addProduct($product) { @@ -90,7 +103,7 @@ public function addProduct($product) $this->_addVisitorToItem($item); $item->loadByProduct($product); - if (!$item->getId()) { + if (!$item->getId() && $this->productExists($product)) { $item->addProductData($product); $item->save(); } @@ -98,6 +111,25 @@ public function addProduct($product) return $this; } + /** + * Check product exists. + * + * @param int|\Magento\Catalog\Model\Product $product + * @return bool + */ + private function productExists($product) + { + if ($product instanceof \Magento\Catalog\Model\Product && $product->getId()) { + return true; + } + try { + $product = $this->productRepository->getById((int)$product); + return !empty($product->getId()); + } catch (NoSuchEntityException $e) { + return false; + } + } + /** * Add products to compare list * diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index 44ebdf0f1f283..a7f7bad1a5167 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -6,7 +6,9 @@ namespace Magento\Catalog\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductFactory; /** * Catalog product copier. @@ -28,7 +30,7 @@ class Copier protected $copyConstructor; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $productFactory; @@ -36,17 +38,24 @@ class Copier * @var \Magento\Framework\EntityManager\MetadataPool */ protected $metadataPool; + /** + * @var ScopeOverriddenValue + */ + private $scopeOverriddenValue; /** * @param CopyConstructorInterface $copyConstructor - * @param \Magento\Catalog\Model\ProductFactory $productFactory + * @param ProductFactory $productFactory + * @param ScopeOverriddenValue $scopeOverriddenValue */ public function __construct( CopyConstructorInterface $copyConstructor, - \Magento\Catalog\Model\ProductFactory $productFactory + ProductFactory $productFactory, + ScopeOverriddenValue $scopeOverriddenValue ) { $this->productFactory = $productFactory; $this->copyConstructor = $copyConstructor; + $this->scopeOverriddenValue = $scopeOverriddenValue; } /** @@ -121,19 +130,20 @@ private function setStoresUrl(Product $product, Product $duplicate) : void $storeIds = $duplicate->getStoreIds(); $productId = $product->getId(); $productResource = $product->getResource(); - $defaultUrlKey = $productResource->getAttributeRawValue( - $productId, - 'url_key', - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); $duplicate->setData('save_rewrites_history', false); foreach ($storeIds as $storeId) { + $useDefault = !$this->scopeOverriddenValue->containsValue( + ProductInterface::class, + $product, + 'url_key', + $storeId + ); + if ($useDefault) { + continue; + } $isDuplicateSaved = false; $duplicate->setStoreId($storeId); $urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId); - if ($urlKey === $defaultUrlKey) { - continue; - } do { $urlKey = $this->modifyUrl($urlKey); $duplicate->setUrlKey($urlKey); diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index e06e85e90a2d8..225a3a4c44a9b 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\Operation\ExtensionInterface; use Magento\MediaStorage\Model\File\Uploader as FileUploader; +use Magento\Store\Model\StoreManagerInterface; /** * Create handler for catalog product gallery @@ -74,6 +79,16 @@ class CreateHandler implements ExtensionInterface */ private $mediaAttributeCodes; + /** + * @var array + */ + private $imagesGallery; + + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository @@ -82,6 +97,8 @@ class CreateHandler implements ExtensionInterface * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb + * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\EntityManager\MetadataPool $metadataPool, @@ -90,7 +107,8 @@ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, \Magento\Framework\Filesystem $filesystem, - \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb + \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { $this->metadata = $metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $this->attributeRepository = $attributeRepository; @@ -99,6 +117,7 @@ public function __construct( $this->mediaConfig = $mediaConfig; $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fileStorageDb = $fileStorageDb; + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -137,9 +156,13 @@ public function execute($product, $arguments = []) if ($product->getIsDuplicate() != true) { foreach ($value['images'] as &$image) { + if (!empty($image['removed']) && !$this->canRemoveImage($product, $image['file'])) { + $image['removed'] = ''; + } + if (!empty($image['removed'])) { $clearImages[] = $image['file']; - } elseif (empty($image['value_id'])) { + } elseif (empty($image['value_id']) || !empty($image['recreate'])) { $newFile = $this->moveImageFromTmp($image['file']); $image['new_file'] = $newFile; $newImages[$image['file']] = $image; @@ -152,6 +175,10 @@ public function execute($product, $arguments = []) // For duplicating we need copy original images. $duplicate = []; foreach ($value['images'] as &$image) { + if (!empty($image['removed']) && !$this->canRemoveImage($product, $image['file'])) { + $image['removed'] = ''; + } + if (empty($image['value_id']) || !empty($image['removed'])) { continue; } @@ -538,4 +565,46 @@ private function processMediaAttributeLabel( ); } } + + /** + * Get product images for all stores + * + * @param ProductInterface $product + * @return array + */ + private function getImagesForAllStores(ProductInterface $product) + { + if ($this->imagesGallery === null) { + $storeIds = array_keys($this->storeManager->getStores()); + $storeIds[] = 0; + + $this->imagesGallery = $this->resourceModel->getProductImages($product, $storeIds); + } + + return $this->imagesGallery; + } + + /** + * Check possibility to remove image + * + * @param ProductInterface $product + * @param string $imageFile + * @return bool + */ + private function canRemoveImage(ProductInterface $product, string $imageFile) :bool + { + $canRemoveImage = true; + $gallery = $this->getImagesForAllStores($product); + $storeId = $product->getStoreId(); + + if (!empty($gallery)) { + foreach ($gallery as $image) { + if ($image['filepath'] === $imageFile && (int) $image['store_id'] !== $storeId) { + $canRemoveImage = false; + } + } + } + + return $canRemoveImage; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index 9e5cf084c25a1..a9afb7cec45e2 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -71,10 +71,12 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { $product = $this->productRepository->save($product); - } catch (InputException $inputException) { - throw $inputException; } catch (\Exception $e) { - throw new StateException(__("The product can't be saved.")); + if ($e instanceof InputException) { + throw $e; + } else { + throw new StateException(__("The product can't be saved.")); + } } foreach ($product->getMediaGalleryEntries() as $entry) { @@ -98,19 +100,13 @@ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) ); } $found = false; + $entryTypes = (array)$entry->getTypes(); foreach ($existingMediaGalleryEntries as $key => $existingEntry) { - $entryTypes = (array)$entry->getTypes(); - $existingEntryTypes = (array)$existingMediaGalleryEntries[$key]->getTypes(); - $existingMediaGalleryEntries[$key]->setTypes(array_diff($existingEntryTypes, $entryTypes)); + $existingEntryTypes = (array)$existingEntry->getTypes(); + $existingEntry->setTypes(array_diff($existingEntryTypes, $entryTypes)); if ($existingEntry->getId() == $entry->getId()) { $found = true; - - $file = $entry->getContent(); - - if ($file && $file->getBase64EncodedData() || $entry->getFile()) { - $entry->setId(null); - } $existingMediaGalleryEntries[$key] = $entry; } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php index e1b788bc3941b..f1d27c38e9456 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php @@ -167,10 +167,10 @@ public function addImage( } $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($pathinfo['basename']); - $dispretionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); - $fileName = $dispretionPath . '/' . $fileName; + $dispersionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); + $fileName = $dispersionPath . '/' . $fileName; - $fileName = $this->getNotDuplicatedFilename($fileName, $dispretionPath); + $fileName = $this->getNotDuplicatedFilename($fileName, $dispersionPath); $destinationFile = $this->mediaConfig->getTmpMediaPath($fileName); @@ -465,27 +465,27 @@ protected function getUniqueFileName($file, $forTmp = false) * Get filename which is not duplicated with other files in media temporary and media directories * * @param string $fileName - * @param string $dispretionPath + * @param string $dispersionPath * @return string * @since 101.0.0 */ - protected function getNotDuplicatedFilename($fileName, $dispretionPath) + protected function getNotDuplicatedFilename($fileName, $dispersionPath) { - $fileMediaName = $dispretionPath . '/' + $fileMediaName = $dispersionPath . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($this->mediaConfig->getMediaPath($fileName)); - $fileTmpMediaName = $dispretionPath . '/' + $fileTmpMediaName = $dispersionPath . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($this->mediaConfig->getTmpMediaPath($fileName)); if ($fileMediaName != $fileTmpMediaName) { if ($fileMediaName != $fileName) { return $this->getNotDuplicatedFilename( $fileMediaName, - $dispretionPath + $dispersionPath ); } elseif ($fileTmpMediaName != $fileName) { return $this->getNotDuplicatedFilename( $fileTmpMediaName, - $dispretionPath + $dispersionPath ); } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 189135776b68b..049846ef36490 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -5,6 +5,7 @@ */ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\EntityManager\Operation\ExtensionInterface; /** @@ -75,6 +76,16 @@ protected function processNewImage($product, array &$image) $image['value_id'], $product->getData($this->metadata->getLinkField()) ); + } elseif (!empty($image['recreate'])) { + $data['value_id'] = $image['value_id']; + $data['value'] = $image['file']; + $data['attribute_id'] = $this->getAttribute()->getAttributeId(); + + if (!empty($image['media_type'])) { + $data['media_type'] = $image['media_type']; + } + + $this->resourceModel->saveDataRow(Gallery::GALLERY_TABLE, $data); } return $data; diff --git a/app/code/Magento/Catalog/Model/Product/Hydrator.php b/app/code/Magento/Catalog/Model/Product/Hydrator.php new file mode 100644 index 0000000000000..dcdce7202b212 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Hydrator.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Framework\EntityManager\HydratorInterface; + +/** + * Class is used to extract data and populate entity with data + */ +class Hydrator implements HydratorInterface +{ + /** + * @inheritdoc + */ + public function extract($entity) + { + return $entity->getData(); + } + + /** + * @inheritdoc + */ + public function hydrate($entity, array $data) + { + $lockedAttributes = $entity->getLockedAttributes(); + $entity->unlockAttributes(); + $entity->setData(array_merge($entity->getData(), $data)); + foreach ($lockedAttributes as $attribute) { + $entity->lockAttribute($attribute); + } + + return $entity; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php index 4b7a623b15c19..c0f4e83ef3de4 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php +++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php @@ -130,10 +130,12 @@ private function getWatermark(string $type, int $scopeId = null): array ); if ($file) { - $size = $this->scopeConfig->getValue( - "design/watermark/{$type}_size", - ScopeInterface::SCOPE_STORE, - $scopeId + $size = explode( + 'x', + $this->scopeConfig->getValue( + "design/watermark/{$type}_size", + ScopeInterface::SCOPE_STORE + ) ); $opacity = $this->scopeConfig->getValue( "design/watermark/{$type}_imageOpacity", @@ -145,8 +147,8 @@ private function getWatermark(string $type, int $scopeId = null): array ScopeInterface::SCOPE_STORE, $scopeId ); - $width = !empty($size['width']) ? $size['width'] : null; - $height = !empty($size['height']) ? $size['height'] : null; + $width = !empty($size['0']) ? $size['0'] : null; + $height = !empty($size['1']) ? $size['1'] : null; return [ 'watermark_file' => $file, diff --git a/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php index a7468bcb1e77f..4a8e6431d6ce8 100644 --- a/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php @@ -4,14 +4,17 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Link; use Magento\Catalog\Api\ProductLinkRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Product\Link; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Api\Data\ProductLinkInterface; /** - * Class SaveProductLinks + * Save product links. */ class SaveHandler { @@ -47,8 +50,10 @@ public function __construct( } /** - * @param string $entityType - * @param object $entity + * Save product links for the product. + * + * @param string $entityType Product type. + * @param \Magento\Catalog\Api\Data\ProductInterface $entity * @return \Magento\Catalog\Api\Data\ProductInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -56,13 +61,13 @@ public function execute($entityType, $entity) { $link = $entity->getData($this->metadataPool->getMetadata($entityType)->getLinkField()); if ($this->linkResource->hasProductLinks($link)) { - /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ foreach ($this->productLinkRepository->getList($entity) as $link) { $this->productLinkRepository->delete($link); } } // Build links per type + /** @var ProductLinkInterface[][] $linksByType */ $linksByType = []; foreach ($entity->getProductLinks() as $link) { $linksByType[$link->getLinkType()][] = $link; @@ -71,13 +76,17 @@ public function execute($entityType, $entity) // Set array position as a fallback position if necessary foreach ($linksByType as $linkType => $links) { if (!$this->hasPosition($links)) { - array_walk($linksByType[$linkType], function ($productLink, $position) { - $productLink->setPosition(++$position); - }); + array_walk( + $linksByType[$linkType], + function (ProductLinkInterface $productLink, $position) { + $productLink->setPosition(++$position); + } + ); } } // Flatten multi-dimensional linksByType in ProductLinks + /** @var ProductLinkInterface[] $productLinks */ $productLinks = array_reduce($linksByType, 'array_merge', []); if (count($productLinks) > 0) { @@ -90,13 +99,14 @@ public function execute($entityType, $entity) /** * Check if at least one link without position - * @param array $links + * + * @param ProductLinkInterface[] $links * @return bool */ - private function hasPosition(array $links) + private function hasPosition(array $links): bool { foreach ($links as $link) { - if (!array_key_exists('position', $link->getData())) { + if ($link->getPosition() === null) { return false; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 4f730834412e4..3a0920fb1c530 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -11,6 +11,11 @@ use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option\Type\Date; +use Magento\Catalog\Model\Product\Option\Type\DefaultType; +use Magento\Catalog\Model\Product\Option\Type\File; +use Magento\Catalog\Model\Product\Option\Type\Select; +use Magento\Catalog\Model\Product\Option\Type\Text; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Framework\EntityManager\MetadataPool; @@ -98,6 +103,16 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ protected $validatorPool; + /** + * @var string[] + */ + private $optionGroups; + + /** + * @var string[] + */ + private $optionTypesToGroups; + /** * @var MetadataPool */ @@ -121,6 +136,8 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory + * @param array $optionGroups + * @param array $optionTypesToGroups * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -135,14 +152,34 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null + ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null, + array $optionGroups = [], + array $optionTypesToGroups = [] ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; - $this->validatorPool = $validatorPool; $this->string = $string; + $this->validatorPool = $validatorPool; $this->customOptionValuesFactory = $customOptionValuesFactory ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + $this->optionGroups = $optionGroups ?: [ + self::OPTION_GROUP_DATE => Date::class, + self::OPTION_GROUP_FILE => File::class, + self::OPTION_GROUP_SELECT => Select::class, + self::OPTION_GROUP_TEXT => Text::class, + ]; + $this->optionTypesToGroups = $optionTypesToGroups ?: [ + self::OPTION_TYPE_FIELD => self::OPTION_GROUP_TEXT, + self::OPTION_TYPE_AREA => self::OPTION_GROUP_TEXT, + self::OPTION_TYPE_FILE => self::OPTION_GROUP_FILE, + self::OPTION_TYPE_DROP_DOWN => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_RADIO => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_CHECKBOX => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_MULTIPLE => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_DATE => self::OPTION_GROUP_DATE, + self::OPTION_TYPE_DATE_TIME => self::OPTION_GROUP_DATE, + self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, + ]; parent::__construct( $context, @@ -314,36 +351,22 @@ public function getGroupByType($type = null) if ($type === null) { $type = $this->getType(); } - $optionGroupsToTypes = [ - self::OPTION_TYPE_FIELD => self::OPTION_GROUP_TEXT, - self::OPTION_TYPE_AREA => self::OPTION_GROUP_TEXT, - self::OPTION_TYPE_FILE => self::OPTION_GROUP_FILE, - self::OPTION_TYPE_DROP_DOWN => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_RADIO => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_CHECKBOX => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_MULTIPLE => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_DATE => self::OPTION_GROUP_DATE, - self::OPTION_TYPE_DATE_TIME => self::OPTION_GROUP_DATE, - self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, - ]; - return $optionGroupsToTypes[$type] ?? ''; + return $this->optionTypesToGroups[$type] ?? ''; } /** * Group model factory * * @param string $type Option type - * @return \Magento\Catalog\Model\Product\Option\Type\DefaultType + * @return DefaultType * @throws LocalizedException */ public function groupFactory($type) { $group = $this->getGroupByType($type); - if (!empty($group)) { - return $this->optionTypeFactory->create( - 'Magento\Catalog\Model\Product\Option\Type\\' . $this->string->upperCaseWords($group) - ); + if (!empty($group) && isset($this->optionGroups[$group])) { + return $this->optionTypeFactory->create($this->optionGroups[$group]); } throw new LocalizedException(__('The option type to get group instance is incorrect.')); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 2b4739ebeb736..6ac48c565e842 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -159,7 +159,7 @@ public function prepareForCart() if ($this->_dateExists()) { if ($this->useCalendar()) { - $timestamp += $this->_localeDate->date($value['date'], null, true, false)->getTimestamp(); + $timestamp += $this->_localeDate->date($value['date'], null, false, false)->getTimestamp(); } else { $timestamp += mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); } diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php index f2da1e770279e..f078349c2a8f4 100644 --- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php +++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php @@ -182,16 +182,19 @@ public function getList($sku, $customerGroupId) : $customerGroupId); $prices = []; - foreach ($product->getData('tier_price') as $price) { - if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) - || ($customerGroupId === 'all' && $price['all_groups']) - ) { - /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ - $tierPrice = $this->priceFactory->create(); - $tierPrice->setValue($price[$priceKey]) - ->setQty($price['price_qty']) - ->setCustomerGroupId($cgi); - $prices[] = $tierPrice; + $tierPrices = $product->getData('tier_price'); + if ($tierPrices !== null) { + foreach ($tierPrices as $price) { + if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) + || ($customerGroupId === 'all' && $price['all_groups']) + ) { + /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ + $tierPrice = $this->priceFactory->create(); + $tierPrice->setValue($price[$priceKey]) + ->setQty($price['price_qty']) + ->setCustomerGroupId($cgi); + $prices[] = $tierPrice; + } } } return $prices; diff --git a/app/code/Magento/Catalog/Model/ProductAttributeSearchResults.php b/app/code/Magento/Catalog/Model/ProductAttributeSearchResults.php new file mode 100644 index 0000000000000..776009089b9aa --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductAttributeSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Product Attribute search results. + */ +class ProductAttributeSearchResults extends SearchResults implements ProductAttributeSearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php index b96aff148e750..7b25533ff72b8 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php +++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php @@ -4,8 +4,11 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\ProductLink; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductLink\Converter\ConverterPool; use Magento\Framework\Exception\NoSuchEntityException; @@ -19,6 +22,11 @@ class CollectionProvider */ protected $providers; + /** + * @var MapProviderInterface[] + */ + private $mapProviders; + /** * @var ConverterPool */ @@ -27,43 +35,169 @@ class CollectionProvider /** * @param ConverterPool $converterPool * @param CollectionProviderInterface[] $providers + * @param MapProviderInterface[] $mapProviders */ - public function __construct(ConverterPool $converterPool, array $providers = []) + public function __construct(ConverterPool $converterPool, array $providers = [], array $mapProviders = []) { $this->converterPool = $converterPool; $this->providers = $providers; + $this->mapProviders = $mapProviders; + } + + /** + * Extract link data from linked products. + * + * @param Product[] $linkedProducts + * @param string $type + * @return array + */ + private function prepareList(array $linkedProducts, string $type): array + { + $converter = $this->converterPool->getConverter($type); + $links = []; + foreach ($linkedProducts as $item) { + $itemId = $item->getId(); + $links[$itemId] = $converter->convert($item); + $links[$itemId]['position'] = $links[$itemId]['position'] ?? 0; + $links[$itemId]['link_type'] = $type; + } + + return $links; } /** * Get product collection by link type * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param string $type * @return array * @throws NoSuchEntityException */ - public function getCollection(\Magento\Catalog\Model\Product $product, $type) + public function getCollection(Product $product, $type) { if (!isset($this->providers[$type])) { throw new NoSuchEntityException(__("The collection provider isn't registered.")); } $products = $this->providers[$type]->getLinkedProducts($product); - $converter = $this->converterPool->getConverter($type); - $sorterItems = []; - foreach ($products as $item) { - $itemId = $item->getId(); - $sorterItems[$itemId] = $converter->convert($item); - $sorterItems[$itemId]['position'] = $sorterItems[$itemId]['position'] ?? 0; + + $linkData = $this->prepareList($products, $type); + usort( + $linkData, + function (array $itemA, array $itemB): int { + $posA = (int)$itemA['position']; + $posB = (int)$itemB['position']; + + return $posA <=> $posB; + } + ); + + return $linkData; + } + + /** + * Load maps from map providers. + * + * @param array $map + * @param array $typeProcessors + * @param Product[] $products + * @return void + */ + private function retrieveMaps(array &$map, array $typeProcessors, array $products): void + { + /** + * @var MapProviderInterface $processor + * @var string[] $types + */ + foreach ($typeProcessors as $processorIndex => $types) { + $typeMap = $this->mapProviders[$processorIndex]->fetchMap($products, $types); + /** + * @var string $sku + * @var Product[][] $links + */ + foreach ($typeMap as $sku => $links) { + $linkData = []; + foreach ($links as $linkType => $linkedProducts) { + $linkData[] = $this->prepareList($linkedProducts, $linkType); + } + if ($linkData) { + $existing = []; + if (array_key_exists($sku, $map)) { + $existing = $map[$sku]; + } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $map[$sku] = array_merge($existing, ...$linkData); + } + } + } + } + + /** + * Load links for each product separately. + * + * @param \SplObjectStorage $map + * @param string[] $types + * @param Product[] $products + * @return void + * @throws NoSuchEntityException + */ + private function retrieveSingles(array &$map, array $types, array $products): void + { + foreach ($products as $product) { + $linkData = []; + foreach ($types as $type) { + $linkData[] = $this->getCollection($product, $type); + } + $linkData = array_filter($linkData); + if ($linkData) { + $existing = []; + if (array_key_exists($product->getSku(), $map)) { + $existing = $map[$product->getSku()]; + } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $map[$product->getSku()] = array_merge($existing, ...$linkData); + } } + } - usort($sorterItems, function ($itemA, $itemB) { - $posA = (int)$itemA['position']; - $posB = (int)$itemB['position']; + /** + * Load map of linked product data. + * + * Link data consists of link_type, type, sku, position, extension attributes? and custom_attributes?. + * + * @param Product[] $products + * @param array $types Keys - string names, values - codes. + * @return array Keys - SKUs, values containing link data. + * @throws NoSuchEntityException + * @throws \InvalidArgumentException + */ + public function getMap(array $products, array $types): array + { + if (!$types) { + throw new \InvalidArgumentException('Types are required'); + } + $map = []; + $typeProcessors = []; + /** @var string[] $singleProcessors */ + $singleProcessors = []; + //Finding map processors + foreach ($types as $type => $typeCode) { + foreach ($this->mapProviders as $i => $mapProvider) { + if ($mapProvider->canProcessLinkType($type)) { + if (!array_key_exists($i, $typeProcessors)) { + $typeProcessors[$i] = []; + } + $typeProcessors[$i][$type] = $typeCode; + continue 2; + } + } + //No map processor found, will process 1 by 1 + $singleProcessors[] = $type; + } - return $posA <=> $posB; - }); + $this->retrieveMaps($map, $typeProcessors, $products); + $this->retrieveSingles($map, $singleProcessors, $products); - return $sorterItems; + return $map; } } diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider/LinkedMapProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider/LinkedMapProvider.php new file mode 100644 index 0000000000000..6be2d2e52cf23 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider/LinkedMapProvider.php @@ -0,0 +1,232 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink\CollectionProvider; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductLink\MapProviderInterface; +use Magento\Catalog\Model\Product\Link; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection as LinkedProductCollection; +use Magento\Catalog\Model\ResourceModel\Product\Link\Product\CollectionFactory as LinkedProductCollectionFactory; + +/** + * Provides linked products. + */ +class LinkedMapProvider implements MapProviderInterface +{ + /** + * Link types supported. + */ + private const TYPES = ['crosssell', 'related', 'upsell']; + + /** + * Type name => Product model cache key. + */ + private const PRODUCT_CACHE_KEY_MAP = [ + 'crosssell' => 'cross_sell_products', + 'upsell' => 'up_sell_products', + 'related' => 'related_products' + ]; + + /** + * @var Link + */ + private $linkModel; + + /** + * @var MetadataPool + */ + private $metadata; + + /** + * @var LinkedProductCollectionFactory + */ + private $productCollectionFactory; + + /** + * LinkedMapProvider constructor. + * @param Link $linkModel + * @param MetadataPool $metadataPool + * @param LinkedProductCollectionFactory $productCollectionFactory + */ + public function __construct( + Link $linkModel, + MetadataPool $metadataPool, + LinkedProductCollectionFactory $productCollectionFactory + ) { + $this->linkModel = $linkModel; + $this->metadata = $metadataPool; + $this->productCollectionFactory = $productCollectionFactory; + } + + /** + * @inheritDoc + */ + public function canProcessLinkType(string $linkType): bool + { + return in_array($linkType, self::TYPES, true); + } + + /** + * Add linked products to the map. + * + * @param Product[][] $map + * @param string $sku + * @param string $type + * @param Product[] $linked + * @return void + */ + private function addLinkedToMap(array &$map, string $sku, string $type, array $linked): void + { + if (!array_key_exists($sku, $map)) { + $map[$sku] = []; + } + if (!array_key_exists($type, $map[$sku])) { + $map[$sku][$type] = []; + } + $map[$sku][$type] = array_merge($map[$sku][$type], $linked); + } + + /** + * Extract cached linked products from entities and find root products that do need a query. + * + * @param Product[] $products Products mapped by link field value. + * @param int[] $types Type requested. + * @param Product[][] $map Map of linked products. + * @return string[][] {Type name => Product link field values} map. + */ + private function processCached(array $products, array $types, array &$map): array + { + /** @var string[][] $query */ + $query = []; + + foreach ($products as $productId => $product) { + $sku = $product->getSku(); + foreach (array_keys($types) as $type) { + if (array_key_exists($type, self::PRODUCT_CACHE_KEY_MAP) + && $product->hasData(self::PRODUCT_CACHE_KEY_MAP[$type]) + ) { + $this->addLinkedToMap($map, $sku, $type, $product->getData(self::PRODUCT_CACHE_KEY_MAP[$type])); + //Cached found, no need to load. + continue; + } + + if (!array_key_exists($type, $query)) { + $query[$type] = []; + } + $query[$type][] = $productId; + } + } + + return $query; + } + + /** + * Load products linked to given products. + * + * @param string[][] $productIds {Type name => Product IDs (link field values)} map. + * @param int[] $types Type name => type ID map. + * @return Product[][] Type name => Product list map. + */ + private function queryLinkedProducts(array $productIds, array $types): array + { + $found = []; + foreach ($types as $type => $typeId) { + if (!array_key_exists($type, $productIds)) { + continue; + } + + /** @var LinkedProductCollection $collection */ + $collection = $this->productCollectionFactory->create(['productIds' => $productIds[$type]]); + $this->linkModel->setLinkTypeId($typeId); + $collection->setLinkModel($this->linkModel); + $collection->setIsStrongMode(); + $found[$type] = $collection->getItems(); + } + + return $found; + } + + /** + * Cache found linked products for existing root product instances. + * + * @param Product[] $forProducts + * @param Product[][] $map + * @param int[] $linkTypesRequested Link types that were queried. + * @return void + */ + private function cacheLinked(array $forProducts, array $map, array $linkTypesRequested): void + { + foreach ($forProducts as $product) { + $sku = $product->getSku(); + if (!array_key_exists($sku, $map)) { + $found = []; + } else { + $found = $map[$sku]; + } + foreach (array_keys($linkTypesRequested) as $linkName) { + if (!array_key_exists($linkName, $found)) { + $found[$linkName] = []; + } + } + + foreach (self::PRODUCT_CACHE_KEY_MAP as $typeName => $cacheKey) { + if (!array_key_exists($typeName, $linkTypesRequested)) { + //If products were not queried for current type then moving on + continue; + } + + $product->setData($cacheKey, $found[$typeName]); + } + } + } + + /** + * @inheritDoc + */ + public function fetchMap(array $products, array $linkTypes): array + { + if (!$products || !$linkTypes) { + throw new \InvalidArgumentException('Products and link types are required.'); + } + + //Gathering products information + $productActualIdField = $this->metadata->getMetadata(ProductInterface::class)->getLinkField(); + /** @var Product[] $rootProducts */ + $rootProducts = []; + /** @var Product $product */ + foreach ($products as $product) { + if ($id = $product->getData($productActualIdField)) { + $rootProducts[$id] = $product; + } + } + unset($product); + //Cannot load without persisted products + if (!$rootProducts) { + return []; + } + + //Finding linked. + $map = []; + $query = $this->processCached($rootProducts, $linkTypes, $map); + $foundLinked = $this->queryLinkedProducts($query, $linkTypes); + + //Filling map with what we've found. + foreach ($foundLinked as $linkType => $linkedProducts) { + foreach ($linkedProducts as $linkedProduct) { + $product = $rootProducts[$linkedProduct->getData('_linked_to_product_id')]; + $this->addLinkedToMap($map, $product->getSku(), $linkType, [$linkedProduct]); + } + } + + $this->cacheLinked($rootProducts, $map, $linkTypes); + + return $map; + } +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteria.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteria.php new file mode 100644 index 0000000000000..4ba59e1fd08e2 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteria.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink\Data; + +use Magento\Catalog\Model\Product; + +/** + * @inheritDoc + */ +class ListCriteria implements ListCriteriaInterface +{ + /** + * @var string + */ + private $productSku; + + /** + * @var Product|null + */ + private $product; + + /** + * @var string[]|null + */ + private $linkTypes; + + /** + * ListCriteria constructor. + * @param string $belongsToProductSku + * @param string[]|null $linkTypes + * @param Product|null $belongsToProduct + */ + public function __construct( + string $belongsToProductSku, + ?array $linkTypes = null, + ?Product $belongsToProduct = null + ) { + $this->productSku = $belongsToProductSku; + $this->linkTypes = $linkTypes; + if ($belongsToProduct) { + $this->productSku = $belongsToProduct->getSku(); + $this->product = $belongsToProduct; + } + } + + /** + * @inheritDoc + */ + public function getBelongsToProductSku(): string + { + return $this->productSku; + } + + /** + * @inheritDoc + */ + public function getLinkTypes(): ?array + { + return $this->linkTypes; + } + + /** + * Product model. + * + * @see getBelongsToProductSku() + * @return Product|null + */ + public function getBelongsToProduct(): ?Product + { + return $this->product; + } +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteriaInterface.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteriaInterface.php new file mode 100644 index 0000000000000..0291be5b9e783 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteriaInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink\Data; + +/** + * Criteria for finding lists. + */ +interface ListCriteriaInterface +{ + /** + * Links belong to this product. + * + * @return string + */ + public function getBelongsToProductSku(): string; + + /** + * Limit links by type (in). + * + * @return string[]|null + */ + public function getLinkTypes(): ?array; +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListResult.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListResult.php new file mode 100644 index 0000000000000..4828837d790fb --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListResult.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink\Data; + +use Magento\Catalog\Api\Data\ProductLinkInterface; + +/** + * @inheritDoc + */ +class ListResult implements ListResultInterface +{ + /** + * @var ProductLinkInterface[]|null + */ + private $result; + + /** + * @var \Throwable|null + */ + private $error; + + /** + * ListResult constructor. + * @param ProductLinkInterface[]|null $result + * @param \Throwable|null $error + */ + public function __construct(?array $result, ?\Throwable $error) + { + $this->result = $result; + $this->error = $error; + if ($this->result === null && $this->error === null) { + throw new \InvalidArgumentException('Result must either contain values or an error.'); + } + } + + /** + * @inheritDoc + */ + public function getResult(): ?array + { + return $this->result; + } + + /** + * @inheritDoc + */ + public function getError(): ?\Throwable + { + return $this->error; + } +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListResultInterface.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListResultInterface.php new file mode 100644 index 0000000000000..f5c0454e7a542 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListResultInterface.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink\Data; + +use Magento\Catalog\Api\Data\ProductLinkInterface; + +/** + * Result of finding a list of links. + */ +interface ListResultInterface +{ + /** + * Found links, null if error occurred. + * + * @return ProductLinkInterface[]|null + */ + public function getResult(): ?array; + + /** + * Error that occurred during retrieval of the list. + * + * @return \Throwable|null + */ + public function getError(): ?\Throwable; +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/MapProviderInterface.php b/app/code/Magento/Catalog/Model/ProductLink/MapProviderInterface.php new file mode 100644 index 0000000000000..31951ab10f5b4 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/MapProviderInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink; + +use Magento\Catalog\Model\Product; + +/** + * Provide link data for products. + */ +interface MapProviderInterface +{ + /** + * Whether a provider can provide data for given link type. + * + * @param string $linkType + * @return bool + */ + public function canProcessLinkType(string $linkType): bool; + + /** + * Load linked products. + * + * Must return map with keys as product objects, values as maps of link types and products linked. + * + * @param Product[] $products With SKUs as keys. + * @param string[] $linkTypes List of supported link types to process, keys - names, values - codes. + * @return Product[][] + */ + public function fetchMap(array $products, array $linkTypes): array; +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php new file mode 100644 index 0000000000000..4bc400605a429 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php @@ -0,0 +1,245 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductLink; + +use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Catalog\Model\ProductLink\Data\ListCriteria; +use Magento\Catalog\Model\ProductLink\Data\ListResult; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SimpleDataObjectConverter; +use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; +use Magento\Catalog\Api\Data\ProductLinkExtensionFactory; +use Magento\Framework\Exception\InputException; + +/** + * Search for product links by criteria. + * + * Batch contract for getting product links. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductLinkQuery +{ + /** + * @var LinkTypeProvider + */ + private $linkTypeProvider; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var CollectionProvider + */ + private $collectionProvider; + + /** + * @var ProductLinkInterfaceFactory + */ + private $productLinkFactory; + + /** + * @var ProductLinkExtensionFactory + */ + private $productLinkExtensionFactory; + + /** + * @param LinkTypeProvider $linkTypeProvider + * @param ProductRepository $productRepository + * @param SearchCriteriaBuilder $criteriaBuilder + * @param CollectionProvider $collectionProvider + * @param ProductLinkInterfaceFactory $productLinkFactory + * @param ProductLinkExtensionFactory $productLinkExtensionFactory + */ + public function __construct( + LinkTypeProvider $linkTypeProvider, + ProductRepository $productRepository, + SearchCriteriaBuilder $criteriaBuilder, + CollectionProvider $collectionProvider, + ProductLinkInterfaceFactory $productLinkFactory, + ProductLinkExtensionFactory $productLinkExtensionFactory + ) { + $this->linkTypeProvider = $linkTypeProvider; + $this->productRepository = $productRepository; + $this->criteriaBuilder = $criteriaBuilder; + $this->collectionProvider = $collectionProvider; + $this->productLinkFactory = $productLinkFactory; + $this->productLinkExtensionFactory = $productLinkExtensionFactory; + } + + /** + * Extract all link types requested. + * + * @param \Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface[] $criteria + * @return string[] + */ + private function extractRequestedLinkTypes(array $criteria): array + { + $linkTypes = $this->linkTypeProvider->getLinkTypes(); + $linkTypesToLoad = []; + foreach ($criteria as $listCriteria) { + if ($listCriteria->getLinkTypes() === null) { + //All link types are to be returned. + $linkTypesToLoad = null; + break; + } + $linkTypesToLoad[] = $listCriteria->getLinkTypes(); + } + if ($linkTypesToLoad !== null) { + if (count($linkTypesToLoad) === 1) { + $linkTypesToLoad = $linkTypesToLoad[0]; + } else { + $linkTypesToLoad = array_merge(...$linkTypesToLoad); + } + $linkTypesToLoad = array_flip($linkTypesToLoad); + $linkTypes = array_filter( + $linkTypes, + function (string $code) use ($linkTypesToLoad) { + return array_key_exists($code, $linkTypesToLoad); + }, + ARRAY_FILTER_USE_KEY + ); + } + + return $linkTypes; + } + + /** + * Load products links were requested for. + * + * @param \Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface[] $criteria + * @return \Magento\Catalog\Model\Product[] Keys are SKUs. + */ + private function loadProductsByCriteria(array $criteria): array + { + $products = []; + $skusToLoad = []; + foreach ($criteria as $listCriteria) { + if ($listCriteria instanceof ListCriteria + && $listCriteria->getBelongsToProduct() + ) { + $products[$listCriteria->getBelongsToProduct()->getSku()] = $listCriteria->getBelongsToProduct(); + } else { + $skusToLoad[] = $listCriteria->getBelongsToProductSku(); + } + } + + $skusToLoad = array_filter( + $skusToLoad, + function ($sku) use ($products) { + return !array_key_exists($sku, $products); + } + ); + if ($skusToLoad) { + $loaded = $this->productRepository->getList( + $this->criteriaBuilder->addFilter('sku', $skusToLoad, 'in')->create() + ); + foreach ($loaded->getItems() as $product) { + $products[$product->getSku()] = $product; + } + } + + return $products; + } + + /** + * Convert links data to DTOs. + * + * @param string $productSku SKU of the root product. + * @param array[] $linksData Links data returned from collection. + * @param string[]|null $acceptedTypes Link types that are accepted. + * @return \Magento\Catalog\Api\Data\ProductLinkInterface[] + */ + private function convertLinksData(string $productSku, array $linksData, ?array $acceptedTypes): array + { + $list = []; + foreach ($linksData as $linkData) { + if ($acceptedTypes && !in_array($linkData['link_type'], $acceptedTypes, true)) { + continue; + } + /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ + $productLink = $this->productLinkFactory->create(); + $productLink->setSku($productSku) + ->setLinkType($linkData['link_type']) + ->setLinkedProductSku($linkData['sku']) + ->setLinkedProductType($linkData['type']) + ->setPosition($linkData['position']); + if (isset($linkData['custom_attributes'])) { + $productLinkExtension = $productLink->getExtensionAttributes(); + if ($productLinkExtension === null) { + /** @var \Magento\Catalog\Api\Data\ProductLinkExtensionInterface $productLinkExtension */ + $productLinkExtension = $this->productLinkExtensionFactory->create(); + } + foreach ($linkData['custom_attributes'] as $option) { + $name = $option['attribute_code']; + $value = $option['value']; + $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name); + // Check if setter exists + if (method_exists($productLinkExtension, $setterName)) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func([$productLinkExtension, $setterName], $value); + } + } + $productLink->setExtensionAttributes($productLinkExtension); + } + $list[] = $productLink; + } + + return $list; + } + + /** + * Get list of product links found by criteria. + * + * Results are returned in the same order as criteria items. + * + * @param \Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface[] $criteria + * @return \Magento\Catalog\Model\ProductLink\Data\ListResultInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function search(array $criteria): array + { + if (!$criteria) { + throw InputException::requiredField('criteria'); + } + + //Requested link types. + $linkTypes = $this->extractRequestedLinkTypes($criteria); + //Requested products. + $products = $this->loadProductsByCriteria($criteria); + //Map of products and their linked products' data. + $map = $this->collectionProvider->getMap($products, $linkTypes); + + //Batch contract results. + $results = []; + foreach ($criteria as $listCriteria) { + $productSku = $listCriteria->getBelongsToProductSku(); + if (!array_key_exists($productSku, $map)) { + $results[] = new ListResult([], null); + continue; + } + try { + $list = $this->convertLinksData($productSku, $map[$productSku], $listCriteria->getLinkTypes()); + $results[] = new ListResult($list, null); + } catch (\Throwable $error) { + $results[] = new ListResult(null, $error); + } + } + + return $results; + } +} diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php index 98977de7effaf..960044efbc2ec 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php +++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\ProductLink; use Magento\Catalog\Api\Data\ProductInterface; @@ -10,6 +13,7 @@ use Magento\Catalog\Api\Data\ProductLinkExtensionFactory; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks as LinksInitializer; use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Catalog\Model\ProductLink\Data\ListCriteria; use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; @@ -17,6 +21,8 @@ use Magento\Framework\App\ObjectManager; /** + * Product link entity repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface @@ -48,11 +54,14 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface /** * @var CollectionProvider + * @deprecated Not used anymore. + * @see query */ protected $entityCollectionProvider; /** * @var LinksInitializer + * @deprecated Not used. */ protected $linkInitializer; @@ -68,14 +77,23 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface /** * @var ProductLinkInterfaceFactory + * @deprecated Not used anymore, search delegated. + * @see getList() */ protected $productLinkFactory; /** * @var ProductLinkExtensionFactory + * @deprecated Not used anymore, search delegated. + * @see getList() */ protected $productLinkExtensionFactory; + /** + * @var ProductLinkQuery + */ + private $query; + /** * Constructor * @@ -86,6 +104,7 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory * @param \Magento\Catalog\Api\Data\ProductLinkExtensionFactory|null $productLinkExtensionFactory + * @param ProductLinkQuery|null $query * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -95,7 +114,8 @@ public function __construct( \Magento\Catalog\Model\ProductLink\Management $linkManagement, \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor, \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null, - \Magento\Catalog\Api\Data\ProductLinkExtensionFactory $productLinkExtensionFactory = null + \Magento\Catalog\Api\Data\ProductLinkExtensionFactory $productLinkExtensionFactory = null, + ?ProductLinkQuery $query = null ) { $this->productRepository = $productRepository; $this->entityCollectionProvider = $entityCollectionProvider; @@ -106,10 +126,11 @@ public function __construct( ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); $this->productLinkExtensionFactory = $productLinkExtensionFactory ?: ObjectManager::getInstance() ->get(\Magento\Catalog\Api\Data\ProductLinkExtensionFactory::class); + $this->query = $query ?? ObjectManager::getInstance()->get(ProductLinkQuery::class); } /** - * {@inheritdoc} + * @inheritDoc */ public function save(\Magento\Catalog\Api\Data\ProductLinkInterface $entity) { @@ -146,47 +167,25 @@ public function save(\Magento\Catalog\Api\Data\ProductLinkInterface $entity) /** * Get product links list * - * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Api\Data\ProductLinkInterface[] */ public function getList(\Magento\Catalog\Api\Data\ProductInterface $product) { - $output = []; - $linkTypes = $this->getLinkTypeProvider()->getLinkTypes(); - foreach (array_keys($linkTypes) as $linkTypeName) { - $collection = $this->entityCollectionProvider->getCollection($product, $linkTypeName); - foreach ($collection as $item) { - /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ - $productLink = $this->productLinkFactory->create(); - $productLink->setSku($product->getSku()) - ->setLinkType($linkTypeName) - ->setLinkedProductSku($item['sku']) - ->setLinkedProductType($item['type']) - ->setPosition($item['position']); - if (isset($item['custom_attributes'])) { - $productLinkExtension = $productLink->getExtensionAttributes(); - if ($productLinkExtension === null) { - $productLinkExtension = $this->productLinkExtensionFactory()->create(); - } - foreach ($item['custom_attributes'] as $option) { - $name = $option['attribute_code']; - $value = $option['value']; - $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name); - // Check if setter exists - if (method_exists($productLinkExtension, $setterName)) { - call_user_func([$productLinkExtension, $setterName], $value); - } - } - $productLink->setExtensionAttributes($productLinkExtension); - } - $output[] = $productLink; - } + if (!$product->getSku() || !$product->getId()) { + return $product->getProductLinks(); } - return $output; + $criteria = new ListCriteria($product->getSku(), null, $product); + $result = $this->query->search([$criteria])[0]; + + if ($result->getError()) { + throw $result->getError(); + } + return $result->getResult(); } /** - * {@inheritdoc} + * @inheritDoc */ public function delete(\Magento\Catalog\Api\Data\ProductLinkInterface $entity) { @@ -219,7 +218,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductLinkInterface $entity) } /** - * {@inheritdoc} + * @inheritDoc */ public function deleteById($sku, $type, $linkedProductSku) { @@ -243,6 +242,8 @@ public function deleteById($sku, $type, $linkedProductSku) } /** + * Get Link resource instance. + * * @return \Magento\Catalog\Model\ResourceModel\Product\Link */ private function getLinkResource() @@ -255,6 +256,8 @@ private function getLinkResource() } /** + * Get LinkTypeProvider instance. + * * @return LinkTypeProvider */ private function getLinkTypeProvider() @@ -267,6 +270,8 @@ private function getLinkTypeProvider() } /** + * Get MetadataPool instance. + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/ProductLink/Search.php b/app/code/Magento/Catalog/Model/ProductLink/Search.php index 8750345aa222b..ad7f3370ab3fe 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/Search.php +++ b/app/code/Magento/Catalog/Model/ProductLink/Search.php @@ -10,7 +10,9 @@ use Magento\Catalog\Api\Data\ProductInterface; -/** Returns collection of product visible in catalog by search key */ +/** + * Returns collection of product visible in catalog by search key + */ class Search { /** @@ -58,7 +60,6 @@ public function prepareCollection( ): \Magento\Catalog\Model\ResourceModel\Product\Collection { $productCollection = $this->productCollectionFactory->create(); $productCollection->addAttributeToSelect(ProductInterface::NAME); - $productCollection->setVisibility($this->catalogVisibility->getVisibleInCatalogIds()); $productCollection->setPage($pageNum, $limit); $this->filter->addFilter($productCollection, 'fulltext', ['fulltext' => $searchKey]); $productCollection->setPage($pageNum, $limit); diff --git a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php index fdcf2956dbdef..2aa92b8f0316e 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php +++ b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php @@ -92,7 +92,17 @@ public function processMediaGallery(ProductInterface $product, array $mediaGalle if ($updatedEntry['file'] === null) { unset($updatedEntry['file']); } - $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); + if (isset($updatedEntry['content'])) { + //need to recreate image and reset object + $existingEntry['recreate'] = true; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $newEntry = array_merge($existingEntry, $updatedEntry); + $newEntries[] = $newEntry; + unset($existingMediaGallery[$key]); + } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); + } } else { //set the removed flag $existingEntry['removed'] = true; diff --git a/app/code/Magento/Catalog/Model/ProductSearchResults.php b/app/code/Magento/Catalog/Model/ProductSearchResults.php new file mode 100644 index 0000000000000..7aa3b4d961c23 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Product search results. + */ +class ProductSearchResults extends SearchResults implements ProductSearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 786cec391c460..c4980c917d069 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -9,13 +9,17 @@ * * @author Magento Core Team <core@magentocommerce.com> */ +declare(strict_types=1); + namespace Magento\Catalog\Model\ResourceModel; use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; -use Magento\Catalog\Model\Category as CategoryEntity; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Api\Data\ProductInterface; /** * Resource model for category entity @@ -92,6 +96,12 @@ class Category extends AbstractResource * @var Processor */ private $indexerProcessor; + + /** + * @var MetadataPool + */ + private $metadataPool; + /** * Category constructor. * @param \Magento\Eav\Model\Entity\Context $context @@ -103,6 +113,7 @@ class Category extends AbstractResource * @param Processor $indexerProcessor * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param MetadataPool|null $metadataPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -114,7 +125,8 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, Processor $indexerProcessor, $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + MetadataPool $metadataPool = null ) { parent::__construct( $context, @@ -129,6 +141,7 @@ public function __construct( $this->indexerProcessor = $indexerProcessor; $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** @@ -275,7 +288,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $object) if ($object->getPosition() === null) { $object->setPosition($this->_getMaxPosition($object->getPath()) + 1); } - $path = explode('/', $object->getPath()); + $path = explode('/', (string)$object->getPath()); $level = count($path) - ($object->getId() ? 1 : 0); $toUpdateChild = array_diff($path, [$object->getId()]); @@ -314,7 +327,7 @@ protected function _afterSave(\Magento\Framework\DataObject $object) /** * Add identifier for new category */ - if (substr($object->getPath(), -1) == '/') { + if (substr((string)$object->getPath(), -1) == '/') { $object->setPath($object->getPath() . $object->getId()); $this->_savePath($object); } @@ -352,7 +365,7 @@ protected function _getMaxPosition($path) { $connection = $this->getConnection(); $positionField = $connection->quoteIdentifier('position'); - $level = count(explode('/', $path)); + $level = count(explode('/', (string)$path)); $bind = ['c_level' => $level, 'c_path' => $path . '/%']; $select = $connection->select()->from( $this->getTable('catalog_category_entity'), @@ -717,7 +730,7 @@ public function getCategories($parent, $recursionLevel = 0, $sorted = false, $as */ public function getParentCategories($category) { - $pathIds = array_reverse(explode(',', $category->getPathInStore())); + $pathIds = array_reverse(explode(',', (string)$category->getPathInStore())); /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categories */ $categories = $this->_categoryCollectionFactory->create(); return $categories->setStore( @@ -758,6 +771,8 @@ public function getParentDesignCategory($category) 'custom_layout_update' )->addAttributeToSelect( 'custom_apply_to_products' + )->addAttributeToSelect( + 'custom_layout_update_file' )->addFieldToFilter( 'entity_id', ['in' => $pathIds] @@ -1134,4 +1149,45 @@ private function getAggregateCount() } return $this->aggregateCount; } + + /** + * Get category with children. + * + * @param int $categoryId + * @return array + */ + public function getCategoryWithChildren(int $categoryId): array + { + $connection = $this->getConnection(); + + $selectAttributeCode = $connection->select() + ->from( + ['eav_attribute' => $this->getTable('eav_attribute')], + ['attribute_id'] + )->where('entity_type_id = ?', CategorySetup::CATEGORY_ENTITY_TYPE_ID) + ->where('attribute_code = ?', 'is_anchor') + ->limit(1); + $isAnchorAttributeCode = $connection->fetchOne($selectAttributeCode); + if (empty($isAnchorAttributeCode) || (int)$isAnchorAttributeCode <= 0) { + return []; + } + + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $select = $connection->select() + ->from( + ['cce' => $this->getTable('catalog_category_entity')], + [$linkField, 'parent_id', 'path'] + )->join( + ['cce_int' => $this->getTable('catalog_category_entity_int')], + 'cce.' . $linkField . ' = cce_int.' . $linkField, + ['is_anchor' => 'cce_int.value'] + )->where( + 'cce_int.attribute_id = ?', + $isAnchorAttributeCode + )->where( + "cce.path LIKE '%/{$categoryId}' OR cce.path LIKE '%/{$categoryId}/%'" + )->order('path'); + + return $connection->fetchAll($select); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 23f612582f42e..a396ae57c2ccd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -236,7 +236,7 @@ public function afterSave() ) { $this->_indexerEavProcessor->markIndexerAsInvalid(); } - + $this->_source = null; return parent::afterSave(); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index b0b15cfd69d13..c5587d3b25665 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -6,10 +6,12 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; +use Magento\Eav\Api\AttributeManagementInterface; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Product as ProductEntity; use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; +use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\Model\AbstractModel; @@ -93,6 +95,11 @@ class Product extends AbstractResource */ private $tableMaintainer; + /** + * @var AttributeManagementInterface + */ + private $eavAttributeManagement; + /** * @param \Magento\Eav\Model\Entity\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -106,7 +113,7 @@ class Product extends AbstractResource * @param array $data * @param TableMaintainer|null $tableMaintainer * @param UniqueValidationInterface|null $uniqueValidator - * + * @param AttributeManagementInterface|null $eavAttributeManagement * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -121,7 +128,8 @@ public function __construct( \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, $data = [], TableMaintainer $tableMaintainer = null, - UniqueValidationInterface $uniqueValidator = null + UniqueValidationInterface $uniqueValidator = null, + AttributeManagementInterface $eavAttributeManagement = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -138,6 +146,8 @@ public function __construct( ); $this->connectionName = 'catalog'; $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); + $this->eavAttributeManagement = $eavAttributeManagement + ?? ObjectManager::getInstance()->get(AttributeManagementInterface::class); } /** @@ -268,10 +278,10 @@ public function getIdBySku($sku) /** * Process product data before save * - * @param \Magento\Framework\DataObject $object + * @param DataObject $object * @return $this */ - protected function _beforeSave(\Magento\Framework\DataObject $object) + protected function _beforeSave(DataObject $object) { $self = parent::_beforeSave($object); /** @@ -286,15 +296,73 @@ protected function _beforeSave(\Magento\Framework\DataObject $object) /** * Save data related with product * - * @param \Magento\Framework\DataObject $product + * @param DataObject $product * @return $this */ - protected function _afterSave(\Magento\Framework\DataObject $product) + protected function _afterSave(DataObject $product) { + $this->removeNotInSetAttributeValues($product); $this->_saveWebsiteIds($product)->_saveCategories($product); return parent::_afterSave($product); } + /** + * Remove attribute values that absent in product attribute set + * + * @param DataObject $product + * @return DataObject + */ + private function removeNotInSetAttributeValues(DataObject $product): DataObject + { + $oldAttributeSetId = $product->getOrigData(ProductEntity::ATTRIBUTE_SET_ID); + if ($oldAttributeSetId && $product->dataHasChangedFor(ProductEntity::ATTRIBUTE_SET_ID)) { + $newAttributes = $product->getAttributes(); + $newAttributesCodes = array_keys($newAttributes); + $oldAttributes = $this->eavAttributeManagement->getAttributes( + ProductEntity::ENTITY, + $oldAttributeSetId + ); + $oldAttributesCodes = []; + foreach ($oldAttributes as $oldAttribute) { + $oldAttributesCodes[] = $oldAttribute->getAttributecode(); + } + $notInSetAttributeCodes = array_diff($oldAttributesCodes, $newAttributesCodes); + if (!empty($notInSetAttributeCodes)) { + $this->deleteSelectedEntityAttributeRows($product, $notInSetAttributeCodes); + } + } + + return $product; + } + + /** + * Clear selected entity attribute rows + * + * @param DataObject $product + * @param array $attributeCodes + * @return void + */ + private function deleteSelectedEntityAttributeRows(DataObject $product, array $attributeCodes): void + { + $backendTables = []; + foreach ($attributeCodes as $attributeCode) { + $attribute = $this->getAttribute($attributeCode); + $backendTable = $attribute->getBackendTable(); + if (!$attribute->isStatic() && $backendTable) { + $backendTables[$backendTable][] = $attribute->getId(); + } + } + + $entityIdField = $this->getLinkField(); + $entityId = $product->getData($entityIdField); + foreach ($backendTables as $backendTable => $attributes) { + $connection = $this->getConnection(); + $where = $connection->quoteInto('attribute_id IN (?)', $attributes); + $where .= $connection->quoteInto(" AND {$entityIdField} = ?", $entityId); + $connection->delete($backendTable, $where); + } + } + /** * @inheritdoc */ @@ -337,12 +405,12 @@ protected function _saveWebsiteIds($product) /** * Save product category relations * - * @param \Magento\Framework\DataObject $object + * @param DataObject $object * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @deprecated 101.1.0 */ - protected function _saveCategories(\Magento\Framework\DataObject $object) + protected function _saveCategories(DataObject $object) { return $this; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index dbd6a7a2e1094..e31180d4ff6cf 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\ResourceModel\Product; @@ -22,6 +23,7 @@ use Magento\Framework\Indexer\DimensionFactory; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Category; /** * Product collection @@ -191,7 +193,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac /** * Catalog data * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager = null; @@ -302,6 +304,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac */ private $urlFinder; + /** + * @var Category + */ + private $categoryResourceModel; + /** * Collection constructor * @@ -315,7 +322,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -330,6 +337,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory + * @param Category|null $categoryResourceModel * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -344,7 +352,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, @@ -358,7 +366,8 @@ public function __construct( MetadataPool $metadataPool = null, TableMaintainer $tableMaintainer = null, PriceTableResolver $priceTableResolver = null, - DimensionFactory $dimensionFactory = null + DimensionFactory $dimensionFactory = null, + Category $categoryResourceModel = null ) { $this->moduleManager = $moduleManager; $this->_catalogProductFlatState = $catalogProductFlatState; @@ -392,6 +401,8 @@ public function __construct( $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get(PriceTableResolver::class); $this->dimensionFactory = $dimensionFactory ?: ObjectManager::getInstance()->get(DimensionFactory::class); + $this->categoryResourceModel = $categoryResourceModel ?: ObjectManager::getInstance() + ->get(Category::class); } /** @@ -1584,6 +1595,8 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = } else { return parent::addAttributeToFilter($attribute, $condition, $joinType); } + + return $this; } /** @@ -1673,7 +1686,11 @@ public function addFilterByRequiredOptions() public function setVisibility($visibility) { $this->_productLimitationFilters['visibility'] = $visibility; - $this->_applyProductLimitations(); + if ($this->getStoreId() == Store::DEFAULT_STORE_ID) { + $this->addAttributeToFilter('visibility', $visibility); + } else { + $this->_applyProductLimitations(); + } return $this; } @@ -2053,12 +2070,13 @@ protected function _applyProductLimitations() protected function _applyZeroStoreProductLimitations() { $filters = $this->_productLimitationFilters; + $categories = $this->getChildrenCategories((int)$filters['category_id']); $conditions = [ 'cat_pro.product_id=e.entity_id', $this->getConnection()->quoteInto( - 'cat_pro.category_id=?', - $filters['category_id'] + 'cat_pro.category_id IN (?)', + $categories ), ]; $joinCond = join(' AND ', $conditions); @@ -2079,6 +2097,40 @@ protected function _applyZeroStoreProductLimitations() return $this; } + /** + * Get children categories. + * + * @param int $categoryId + * @return array + */ + private function getChildrenCategories(int $categoryId): array + { + $categoryIds[] = $categoryId; + $anchorCategory = []; + + $categories = $this->categoryResourceModel->getCategoryWithChildren($categoryId); + if (empty($categories)) { + return $categoryIds; + } + + $firstCategory = array_shift($categories); + if ($firstCategory['is_anchor'] == 1) { + $linkField = $this->getProductEntityMetadata()->getLinkField(); + $anchorCategory[] = (int)$firstCategory[$linkField]; + foreach ($categories as $category) { + if (in_array($category['parent_id'], $categoryIds) + && in_array($category['parent_id'], $anchorCategory)) { + $categoryIds[] = (int)$category[$linkField]; + if ($category['is_anchor'] == 1) { + $anchorCategory[] = (int)$category[$linkField]; + } + } + } + } + + return $categoryIds; + } + /** * Add category ids to loaded items * @@ -2468,10 +2520,10 @@ public function getPricesCount() /** * Add is_saleable attribute to filter * - * @param array|null $condition + * @param mixed $condition * @return $this */ - private function addIsSaleableAttributeToFilter(?array $condition): self + private function addIsSaleableAttributeToFilter($condition): self { $columns = $this->getSelect()->getPart(Select::COLUMNS); foreach ($columns as $columnEntry) { @@ -2499,10 +2551,10 @@ private function addIsSaleableAttributeToFilter(?array $condition): self * Add tier price attribute to filter * * @param string $attribute - * @param array|null $condition + * @param mixed $condition * @return $this */ - private function addTierPriceAttributeToFilter(string $attribute, ?array $condition): self + private function addTierPriceAttributeToFilter(string $attribute, $condition): self { $attrCode = $attribute; $connection = $this->getConnection(); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php index dc3411743a066..92741cf9ba88e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php @@ -64,7 +64,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -76,6 +76,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem * @param \Magento\Catalog\Helper\Product\Compare $catalogProductCompare * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -89,7 +90,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index 7730d7cc9a7fd..e625e38b59f31 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -8,6 +8,8 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; /** * Catalog Product Eav Select and Multiply Select Attributes Indexer resource model @@ -199,13 +201,52 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'dd.attribute_id', 's.store_id', 'value' => new \Zend_Db_Expr('COALESCE(ds.value, dd.value)'), - 'cpe.entity_id', + 'cpe.entity_id AS source_id', ] ); if ($entityIds !== null) { $ids = implode(',', array_map('intval', $entityIds)); + $selectWithoutDefaultStore = $connection->select()->from( + ['wd' => $this->getTable('catalog_product_entity_int')], + [ + 'cpe.entity_id', + 'attribute_id', + 'store_id', + 'value', + 'cpe.entity_id', + ] + )->joinLeft( + ['cpe' => $this->getTable('catalog_product_entity')], + "cpe.{$productIdField} = wd.{$productIdField}", + [] + )->joinLeft( + ['d2d' => $this->getTable('catalog_product_entity_int')], + sprintf( + "d2d.store_id = 0 AND d2d.{$productIdField} = wd.{$productIdField} AND d2d.attribute_id = %s", + $this->_eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId() + ), + [] + )->joinLeft( + ['d2s' => $this->getTable('catalog_product_entity_int')], + "d2s.store_id != 0 AND d2s.attribute_id = d2d.attribute_id AND " . + "d2s.{$productIdField} = d2d.{$productIdField}", + [] + ) + ->where((new \Zend_Db_Expr('COALESCE(d2s.value, d2d.value)')) . ' = ' . ProductStatus::STATUS_ENABLED) + ->where("wd.attribute_id IN({$attrIdsFlat})") + ->where('wd.value IS NOT NULL') + ->where('wd.store_id != 0') + ->where("cpe.entity_id IN({$ids})"); $select->where("cpe.entity_id IN({$ids})"); + $selects = new UnionExpression( + [$select, $selectWithoutDefaultStore], + Select::SQL_UNION, + '( %s )' + ); + + $select = $connection->select(); + $select->from(['u' => $selects]); } /** @@ -342,7 +383,7 @@ private function getMultiSelectAttributeWithSourceModels($attrIds) ProductAttributeInterface::ENTITY_TYPE_CODE, $criteria )->getItems(); - + $options = []; foreach ($attributes as $attribute) { $sourceModelOptions = $attribute->getOptions(); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 9643f4c3a7181..b64cca4ff1b26 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -40,7 +40,7 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface /** * Core data * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -73,7 +73,7 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param string|null $connectionName * @param IndexTableStructureFactory $indexTableStructureFactory * @param PriceModifierInterface[] $priceModifiers @@ -83,7 +83,7 @@ public function __construct( \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, $connectionName = null, IndexTableStructureFactory $indexTableStructureFactory = null, array $priceModifiers = [] diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index a3f463d53e7a8..77407ed699fbd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -37,7 +37,7 @@ class BaseFinalPrice private $joinAttributeProcessor; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManager; @@ -69,7 +69,7 @@ class BaseFinalPrice /** * @param \Magento\Framework\App\ResourceConnection $resource * @param JoinAttributeProcessor $joinAttributeProcessor - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param string $connectionName @@ -77,7 +77,7 @@ class BaseFinalPrice public function __construct( \Magento\Framework\App\ResourceConnection $resource, JoinAttributeProcessor $joinAttributeProcessor, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Framework\EntityManager\MetadataPool $metadataPool, $connectionName = 'indexer' diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php index 5724496d7ebdc..f1d4552cf37f0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php @@ -5,6 +5,14 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product\Link\Product; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\ResourceModel\Category; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionFactory; + /** * Catalog product linked products collection * @@ -50,6 +58,111 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ protected $_hasLinkFilter = false; + /** + * @var string[]|null Root product link fields values. + */ + private $productIds; + + /** + * @var string|null + */ + private $linkField; + + /** + * Collection constructor. + * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Eav\Model\Config $eavConfig + * @param \Magento\Framework\App\ResourceConnection $resource + * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory + * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper + * @param \Magento\Framework\Validator\UniversalFactory $universalFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Module\Manager $moduleManager + * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory + * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Framework\Stdlib\DateTime $dateTime + * @param GroupManagementInterface $groupManagement + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection + * @param ProductLimitationFactory|null $productLimitationFactory + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory + * @param Category|null $categoryResourceModel + * @param string[]|null $productIds Root product IDs (linkFields, not entity_ids). + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\Data\Collection\EntityFactory $entityFactory, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Eav\Model\Config $eavConfig, + \Magento\Framework\App\ResourceConnection $resource, + \Magento\Eav\Model\EntityFactory $eavEntityFactory, + \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, + \Magento\Framework\Validator\UniversalFactory $universalFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Module\Manager $moduleManager, + \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, + \Magento\Catalog\Model\ResourceModel\Url $catalogUrl, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, + \Magento\Customer\Model\Session $customerSession, + \Magento\Framework\Stdlib\DateTime $dateTime, + GroupManagementInterface $groupManagement, + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null, + Category $categoryResourceModel = null, + ?array $productIds = null + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $eavConfig, + $resource, + $eavEntityFactory, + $resourceHelper, + $universalFactory, + $storeManager, + $moduleManager, + $catalogProductFlatState, + $scopeConfig, + $productOptionFactory, + $catalogUrl, + $localeDate, + $customerSession, + $dateTime, + $groupManagement, + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer, + $priceTableResolver, + $dimensionFactory, + $categoryResourceModel + ); + + if ($productIds) { + $this->productIds = $productIds; + $this->_hasLinkFilter = true; + } + } + /** * Declare link model and initialize type attributes join * @@ -98,6 +211,7 @@ public function setProduct(\Magento\Catalog\Model\Product $product) if ($product && $product->getId()) { $this->_hasLinkFilter = true; $this->setStore($product->getStore()); + $this->productIds = [$product->getData($this->getLinkField())]; } return $this; } @@ -142,7 +256,7 @@ public function addProductFilter($products) if (!is_array($products)) { $products = [$products]; } - $identifierField = $this->getProductEntityMetadata()->getIdentifierField(); + $identifierField = $this->getLinkField(); $this->getSelect()->where("product_entity_table.$identifierField IN (?)", $products); $this->_hasLinkFilter = true; } @@ -202,21 +316,20 @@ protected function _joinLinks() $connection->quoteInto('links.link_type_id = ?', $this->_linkTypeId), ]; $joinType = 'join'; - $linkField = $this->getProductEntityMetadata()->getLinkField(); - if ($this->getProduct() && $this->getProduct()->getId()) { - $linkFieldId = $this->getProduct()->getData( - $linkField - ); + $linkField = $this->getLinkField(); + if ($this->productIds) { if ($this->_isStrongMode) { - $this->getSelect()->where('links.product_id = ?', (int)$linkFieldId); + $this->getSelect()->where('links.product_id in (?)', $this->productIds); } else { $joinType = 'joinLeft'; - $joinCondition[] = $connection->quoteInto('links.product_id = ?', $linkFieldId); + $joinCondition[] = $connection->quoteInto('links.product_id in (?)', $this->productIds); + } + if (count($this->productIds) === 1) { + $this->addFieldToFilter( + $linkField, + ['neq' => array_values($this->productIds)[0]] + ); } - $this->addFieldToFilter( - $linkField, - ['neq' => $linkFieldId] - ); } elseif ($this->_isStrongMode) { $this->addFieldToFilter( $linkField, @@ -227,7 +340,7 @@ protected function _joinLinks() $select->{$joinType}( ['links' => $this->getTable('catalog_product_link')], implode(' AND ', $joinCondition), - ['link_id'] + ['link_id' => 'link_id', '_linked_to_product_id' => 'product_id'] ); $this->joinAttributes(); } @@ -347,13 +460,14 @@ public function addLinkAttributeToFilter($code, $condition) /** * Join Product To Links + * * @return void */ private function joinProductsToLinks() { if ($this->_hasLinkFilter) { $metaDataPool = $this->getProductEntityMetadata(); - $linkField = $metaDataPool->getLinkField(); + $linkField = $this->getLinkField(); $entityTable = $metaDataPool->getEntityTable(); $this->getSelect() ->join( @@ -363,4 +477,18 @@ private function joinProductsToLinks() ); } } + + /** + * Get product entity's identifier field. + * + * @return string + */ + private function getLinkField(): string + { + if (!$this->linkField) { + $this->linkField = $this->getProductEntityMetadata()->getLinkField(); + } + + return $this->linkField; + } } diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php index dfae9f4b0da9c..c547ec612bb94 100644 --- a/app/code/Magento/Catalog/Model/View/Asset/Image.php +++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php @@ -88,7 +88,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getUrl() { @@ -96,7 +96,7 @@ public function getUrl() } /** - * {@inheritdoc} + * @inheritdoc */ public function getContentType() { @@ -104,7 +104,7 @@ public function getContentType() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPath() { @@ -112,7 +112,7 @@ public function getPath() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSourceFile() { @@ -131,7 +131,7 @@ public function getSourceContentType() } /** - * {@inheritdoc} + * @inheritdoc */ public function getContent() { @@ -139,7 +139,7 @@ public function getContent() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFilePath() { @@ -147,7 +147,8 @@ public function getFilePath() } /** - * {@inheritdoc} + * @inheritdoc + * * @return ContextInterface */ public function getContext() @@ -156,7 +157,7 @@ public function getContext() } /** - * {@inheritdoc} + * @inheritdoc */ public function getModule() { @@ -191,20 +192,21 @@ private function getImageInfo() /** * Converting bool into a string representation - * @param $miscParams + * + * @param array $miscParams * @return array */ - private function convertToReadableFormat($miscParams) + private function convertToReadableFormat(array $miscParams) { $miscParams['image_height'] = 'h:' . ($miscParams['image_height'] ?? 'empty'); $miscParams['image_width'] = 'w:' . ($miscParams['image_width'] ?? 'empty'); $miscParams['quality'] = 'q:' . ($miscParams['quality'] ?? 'empty'); $miscParams['angle'] = 'r:' . ($miscParams['angle'] ?? 'empty'); - $miscParams['keep_aspect_ratio'] = (isset($miscParams['keep_aspect_ratio']) ? '' : 'non') . 'proportional'; - $miscParams['keep_frame'] = (isset($miscParams['keep_frame']) ? '' : 'no') . 'frame'; - $miscParams['keep_transparency'] = (isset($miscParams['keep_transparency']) ? '' : 'no') . 'transparency'; - $miscParams['constrain_only'] = (isset($miscParams['constrain_only']) ? 'do' : 'not') . 'constrainonly'; - $miscParams['background'] = isset($miscParams['background']) + $miscParams['keep_aspect_ratio'] = (!empty($miscParams['keep_aspect_ratio']) ? '' : 'non') . 'proportional'; + $miscParams['keep_frame'] = (!empty($miscParams['keep_frame']) ? '' : 'no') . 'frame'; + $miscParams['keep_transparency'] = (!empty($miscParams['keep_transparency']) ? '' : 'no') . 'transparency'; + $miscParams['constrain_only'] = (!empty($miscParams['constrain_only']) ? 'do' : 'not') . 'constrainonly'; + $miscParams['background'] = !empty($miscParams['background']) ? 'rgb' . implode(',', $miscParams['background']) : 'nobackground'; return $miscParams; diff --git a/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php b/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php new file mode 100644 index 0000000000000..94977485b95b3 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Observer; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category\Authorization; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\AuthorizationException; + +/** + * Employ additional authorization logic when a category is saved. + */ +class CategoryDesignAuthorization implements ObserverInterface +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * @inheritDoc + * + * @throws AuthorizationException + */ + public function execute(Observer $observer) + { + /** @var CategoryInterface $category */ + $category = $observer->getEvent()->getData('category'); + $this->authorization->authorizeSavingOf($category); + } +} diff --git a/app/code/Magento/Catalog/Observer/FlushCategoryPagesCache.php b/app/code/Magento/Catalog/Observer/FlushCategoryPagesCache.php new file mode 100644 index 0000000000000..751fa3fdfad84 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/FlushCategoryPagesCache.php @@ -0,0 +1,60 @@ +<?php declare(strict_types=1); +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Observer; + +use Magento\Catalog\Model\Category; +use Magento\Framework\Event\Observer as Event; +use Magento\Framework\Event\ObserverInterface; +use Magento\PageCache\Model\Cache\Type as PageCache; +use Magento\PageCache\Model\Config as CacheConfig; + +/** + * Flush the built in page cache when a category is moved + */ +class FlushCategoryPagesCache implements ObserverInterface +{ + + /** + * @var CacheConfig + */ + private $cacheConfig; + + /** + * + * @var PageCache + */ + private $pageCache; + + /** + * FlushCategoryPagesCache constructor. + * + * @param CacheConfig $cacheConfig + * @param PageCache $pageCache + */ + public function __construct(CacheConfig $cacheConfig, PageCache $pageCache) + { + $this->cacheConfig = $cacheConfig; + $this->pageCache = $pageCache; + } + + /** + * Clean the category page cache if built in cache page cache is used. + * + * The built in cache requires cleaning all pages that contain the top category navigation menu when a + * category is moved. This is because the built in cache does not support ESI blocks. + * + * @param Event $event + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute(Event $event) + { + if ($this->cacheConfig->getType() == CacheConfig::BUILT_IN && $this->cacheConfig->isEnabled()) { + $this->pageCache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [Category::CACHE_TAG]); + } + } +} diff --git a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php index 44f9193ab4012..b4aa5bd960b01 100644 --- a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php +++ b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Plugin\Block; use Magento\Catalog\Model\Category; @@ -156,12 +158,13 @@ private function getCurrentCategory() */ private function getCategoryAsArray($category, $currentCategory, $isParentActive) { + $categoryId = $category->getId(); return [ 'name' => $category->getName(), - 'id' => 'category-node-' . $category->getId(), + 'id' => 'category-node-' . $categoryId, 'url' => $this->catalogCategory->getCategoryUrl($category), - 'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true), - 'is_active' => $category->getId() == $currentCategory->getId(), + 'has_active' => in_array((string)$categoryId, explode('/', (string)$currentCategory->getPath()), true), + 'is_active' => $categoryId == $currentCategory->getId(), 'is_category' => true, 'is_parent_active' => $isParentActive ]; @@ -193,4 +196,22 @@ protected function getCategoryTree($storeId, $rootId) return $collection; } + + /** + * Add active + * + * @param \Magento\Theme\Block\Html\Topmenu $subject + * @param string[] $result + * @return string[] + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetCacheKeyInfo(\Magento\Theme\Block\Html\Topmenu $subject, array $result) + { + $activeCategory = $this->getCurrentCategory(); + if ($activeCategory) { + $result[] = Category::CACHE_TAG . '_' . $activeCategory->getId(); + } + + return $result; + } } diff --git a/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php new file mode 100644 index 0000000000000..af2dccb96f937 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category\Authorization; +use Magento\Framework\Exception\LocalizedException; + +/** + * Perform additional authorization for category operations. + */ +class CategoryAuthorization +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * Authorize saving of a category. + * + * @param CategoryRepositoryInterface $subject + * @param CategoryInterface $category + * @throws LocalizedException + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(CategoryRepositoryInterface $subject, CategoryInterface $category): array + { + $this->authorization->authorizeSavingOf($category); + + return [$category]; + } +} diff --git a/app/code/Magento/Catalog/Plugin/ProductAuthorization.php b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php new file mode 100644 index 0000000000000..ce2fe19cf1aee --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Authorization; +use Magento\Framework\Exception\LocalizedException; + +/** + * Perform additional authorization for product operations. + */ +class ProductAuthorization +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * Authorize saving of a product. + * + * @param ProductRepositoryInterface $subject + * @param ProductInterface $product + * @param bool $saveOptions + * @throws LocalizedException + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave( + ProductRepositoryInterface $subject, + ProductInterface $product, + $saveOptions = false + ): array { + $this->authorization->authorizeSavingOf($product); + + return [$product, $saveOptions]; + } +} diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/UpdateCustomLayoutAttributes.php b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateCustomLayoutAttributes.php new file mode 100644 index 0000000000000..81fd35ccd3178 --- /dev/null +++ b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateCustomLayoutAttributes.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Catalog\Setup\CategorySetupFactory; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Category; + +/** + * Add new custom layout related attributes. + */ +class UpdateCustomLayoutAttributes implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var CategorySetupFactory + */ + private $categorySetupFactory; + + /** + * PatchInitial constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + * @param CategorySetupFactory $categorySetupFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + CategorySetupFactory $categorySetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritDoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return []; + } + + /** + * @inheritDoc + */ + public function apply() + { + /** @var CategorySetup $eavSetup */ + $eavSetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]); + $eavSetup->addAttribute( + Product::ENTITY, + 'custom_layout_update_file', + [ + 'type' => 'varchar', + 'label' => 'Custom Layout Update', + 'input' => 'select', + 'source' => \Magento\Catalog\Model\Product\Attribute\Source\LayoutUpdate::class, + 'required' => false, + 'sort_order' => 51, + 'backend' => \Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate::class, + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, + 'group' => 'Design', + 'is_used_in_grid' => false, + 'is_visible_in_grid' => false, + 'is_filterable_in_grid' => false + ] + ); + + $eavSetup->addAttribute( + Category::ENTITY, + 'custom_layout_update_file', + [ + 'type' => 'varchar', + 'label' => 'Custom Layout Update', + 'input' => 'select', + 'source' => \Magento\Catalog\Model\Category\Attribute\Source\LayoutUpdate::class, + 'required' => false, + 'sort_order' => 51, + 'backend' => \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::class, + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, + 'group' => 'Custom Design', + 'is_used_in_grid' => false, + 'is_visible_in_grid' => false, + 'is_filterable_in_grid' => false + ] + ); + + $eavSetup->updateAttribute( + Product::ENTITY, + 'custom_layout_update', + 'is_visible', + false + ); + + $eavSetup->updateAttribute( + Category::ENTITY, + 'custom_layout_update', + 'is_visible', + false + ); + } +} diff --git a/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index c39247f9b30df..0000000000000 --- a/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tables = [ - 'catalog_product_index_price_cfg_opt_agr_tmp', - 'catalog_product_index_price_cfg_opt_tmp', - 'catalog_product_index_price_final_tmp', - 'catalog_product_index_price_opt_tmp', - 'catalog_product_index_price_opt_agr_tmp', - 'catalog_product_index_eav_tmp', - 'catalog_product_index_eav_decimal_tmp', - 'catalog_product_index_price_tmp', - 'catalog_category_product_index_tmp', - ]; - foreach ($tables as $table) { - $tableName = $this->schemaSetup->getTable($table); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductEditPageCreateAttributeSaveInAttributeSetPopUpShownActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductEditPageCreateAttributeSaveInAttributeSetPopUpShownActionGroup.xml new file mode 100644 index 0000000000000..602bd494d599c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductEditPageCreateAttributeSaveInAttributeSetPopUpShownActionGroup.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="AdminAssertProductEditPageCreateAttributeSaveInAttributeSetPopUpShownActionGroup"> + <annotations> + <description>Asserts that after click on "Save In New Attribute Set" poup shown .</description> + </annotations> + <see userInput="Enter Name for New Attribute Set" stepKey="seeContent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml index c6492754515fb..e5cefda0aca96 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml @@ -15,8 +15,8 @@ <arguments> <argument name="image"/> </arguments> - - <conditionalClick selector="{{AdminProductImagesSection.productImagesToggleState('closed')}}" dependentSelector="{{AdminProductImagesSection.productImagesToggleState('open')}}" visible="false" stepKey="clickSectionImage"/> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageFile(image.fileName)}}" visible="false" stepKey="expandImages"/> + <waitForElementVisible selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="seeProductImageName"/> <click selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="clickProductImage"/> <waitForElementVisible selector="{{AdminProductImagesSection.altText}}" stepKey="seeAltTextSection"/> <checkOption selector="{{AdminProductImagesSection.roleBase}}" stepKey="checkRoleBase"/> @@ -25,4 +25,14 @@ <checkOption selector="{{AdminProductImagesSection.roleSwatch}}" stepKey="checkRoleSwatch"/> <click selector="{{AdminSlideOutDialogSection.closeButton}}" stepKey="clickCloseButton"/> </actionGroup> + <actionGroup name="AdminAssignImageRolesIfUnassignedActionGroup" extends="AdminAssignImageRolesActionGroup"> + <annotations> + <description>Requires the navigation to the Product Creation page. Assign the Base, Small, Thumbnail, and Swatch Roles to image.</description> + </annotations> + + <conditionalClick selector="{{AdminProductImagesSection.roleBase}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Base')}}" visible="false" stepKey="checkRoleBase"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSmall}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Small')}}" visible="false" stepKey="checkRoleSmall"/> + <conditionalClick selector="{{AdminProductImagesSection.roleThumbnail}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Thumbnail')}}" visible="false" stepKey="checkRoleThumbnail"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSwatch}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Swatch')}}" visible="false" stepKey="checkRoleSwatch"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index 12bf5179a07d0..afc332cc28378 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -99,7 +99,11 @@ <attachFile selector="{{AdminCategoryContentSection.uploadImageFile}}" userInput="{{image.file}}" stepKey="uploadFile"/> <waitForAjaxLoad time="30" stepKey="waitForAjaxUpload"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryContentSection.imageFileName}}" userInput="{{image.file}}" stepKey="seeImage"/> + <grabTextFrom selector="{{AdminCategoryContentSection.imageFileName}}" stepKey="grabCategoryFileName"/> + <assertRegExp stepKey="assertEquals" message="pass"> + <expectedResult type="string">/magento-logo(_[0-9]+)*?\.png$/</expectedResult> + <actualResult type="variable">grabCategoryFileName</actualResult> + </assertRegExp> </actionGroup> <!-- Remove image from category --> @@ -128,7 +132,11 @@ <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.uploadButton}}" visible="false" stepKey="openContentSection"/> <waitForPageLoad stepKey="waitForPageLoad"/> <waitForElementVisible selector="{{AdminCategoryContentSection.uploadButton}}" stepKey="seeImageSectionIsReady"/> - <see selector="{{AdminCategoryContentSection.imageFileName}}" userInput="{{image.file}}" stepKey="seeImage"/> + <grabTextFrom selector="{{AdminCategoryContentSection.imageFileName}}" stepKey="grabCategoryFileName"/> + <assertRegExp stepKey="assertEquals" message="pass"> + <expectedResult type="string">/magento-logo(_[0-9]+)*?\.png$/</expectedResult> + <actualResult type="variable">grabCategoryFileName</actualResult> + </assertRegExp> </actionGroup> <!-- Action to navigate to Media Gallery. Used in tests to cleanup uploaded images --> @@ -183,6 +191,18 @@ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="dontSeeCategoryInTree"/> </actionGroup> + <actionGroup name="AdminDeleteCategoryByName" extends="DeleteCategory"> + <arguments> + <argument name="categoryName" type="string" defaultValue="category1"/> + </arguments> + <remove keyForRemoval="clickCategoryLink"/> + <remove keyForRemoval="dontSeeCategoryInTree"/> + <remove keyForRemoval="expandToSeeAllCategories"/> + <conditionalClick selector="{{AdminCategorySidebarTreeSection.expandAll}}" dependentSelector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" visible="false" stepKey="expandCategories" after="waitForCategoryPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" stepKey="clickCategory" after="expandCategories"/> + <conditionalClick selector="{{AdminCategorySidebarTreeSection.expandAll}}" dependentSelector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" visible="false" stepKey="expandCategoriesToSeeAll" after="seeDeleteSuccess"/> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" stepKey="dontSeeCategory" after="expandCategoriesToSeeAll"/> + </actionGroup> <!-- Actions to fill out a new category from the product page--> <!-- The action assumes that you are already on an admin product configuration page --> @@ -396,4 +416,44 @@ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> <waitForPageLoad stepKey="waitForPageToLoad1"/> </actionGroup> + + <actionGroup name="AdminCategoryAssignProduct"> + <annotations> + <description>Requires navigation to category creation/edit page. Assign products to category - using "Products in Category" tab.</description> + </annotations> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + + <conditionalClick selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="false" stepKey="clickOnProductInCategory"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickOnResetFilter"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnSku}}" userInput="{{productSku}}" stepKey="fillSkuFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + </actionGroup> + + <actionGroup name="DeleteDefaultCategoryChildren"> + <annotations> + <description>Deletes all children categories of Default Root Category.</description> + </annotations> + + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategoryPage"/> + <executeInSelenium function="function ($webdriver) use ($I) { + $children = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::xpath('//ul[contains(@class, \'x-tree-node-ct\')]/li[@class=\'x-tree-node\' and contains(., + \'{{DefaultCategory.name}}\')]/ul[contains(@class, \'x-tree-node-ct\')]/li//a')); + while (!empty($children)) { + $I->click('//ul[contains(@class, \'x-tree-node-ct\')]/li[@class=\'x-tree-node\' and contains(., + \'{{DefaultCategory.name}}\')]/ul[contains(@class, \'x-tree-node-ct\')]/li//a'); + $I->waitForPageLoad(30); + $I->click('#delete'); + $I->waitForElementVisible('aside.confirm .modal-footer button.action-accept'); + $I->click('aside.confirm .modal-footer button.action-accept'); + $I->waitForPageLoad(30); + $I->waitForElementVisible('#messages div.message-success', 30); + $I->see('You deleted the category.', '#messages div.message-success'); + $children = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::xpath('//ul[contains(@class, \'x-tree-node-ct\')]/li[@class=\'x-tree-node\' and contains(., + \'{{DefaultCategory.name}}\')]/ul[contains(@class, \'x-tree-node-ct\')]/li//a')); + } + }" stepKey="deleteAllChildCategories"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickAddAttributeOnProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickAddAttributeOnProductEditPageActionGroup.xml new file mode 100644 index 0000000000000..488d905a8dc40 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickAddAttributeOnProductEditPageActionGroup.xml @@ -0,0 +1,20 @@ +<?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="AdminClickAddAttributeOnProductEditPageActionGroup"> + <annotations> + <description>Clicks on 'Add Attribute'. Admin Product creation/edit page .</description> + </annotations> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForSidePanel"/> + <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickCreateNewAttributeFromProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickCreateNewAttributeFromProductEditPageActionGroup.xml new file mode 100644 index 0000000000000..658c76ebabeee --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickCreateNewAttributeFromProductEditPageActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AdminClickCreateNewAttributeFromProductEditPageActionGroup"> + <annotations> + <description>Clicks on 'Create New Attribute'. Admin Product creation/edit page .</description> + </annotations> + <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <waitForPageLoad stepKey="waitForSidePanel"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml index 60438e23e084c..a0cff133cbbed 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml @@ -18,4 +18,15 @@ <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> </actionGroup> + + <!-- ActionGroup click on Advanced Inventory Button in product form; + You must already be on the product form page --> + <actionGroup name="AdminClickOnAdvancedInventoryButtonActionGroup"> + <annotations> + <description>Clicks on the 'Advanced Inventory' link on the Admin Product creation/edit page.</description> + </annotations> + + <click selector="{{AdminProductFormSection.advancedInventoryButton}}" stepKey="clickOnAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index 45e2ed6205b20..f3ebbc6370f87 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -16,7 +16,27 @@ <selectOption selector="{{AdminCatalogProductWidgetSection.productAttributesToShow}}" parameterArray="['Name', 'Image', 'Price']" stepKey="selectAllProductAttributes"/> <selectOption selector="{{AdminCatalogProductWidgetSection.productButtonsToShow}}" parameterArray="['Add to Cart', 'Add to Compare', 'Add to Wishlist']" stepKey="selectAllProductButtons"/> <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> + + <actionGroup name="AdminCreateCatalogProductWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateWidgetActionGroup. Creates Catalog Category Link Widget.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string" defaultValue="{{DefaultCategory.name}}"/> + </arguments> + + <waitForElementVisible selector="{{AdminNewWidgetSection.selectCategory}}" after="clickWidgetOptions" stepKey="waitForSelectCategoryButtonVisible"/> + <click selector="{{AdminNewWidgetSection.selectCategory}}" stepKey="clickOnSelectCategory"/> + <waitForPageLoad stepKey="waitForCategoryTreeLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandRootCategoryByName(DefaultCategory.name)}}" stepKey="clickToExpandDefaultCategory"/> + <waitForElementVisible selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="waitForCategoryVisible"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForWidgetPageLoaded"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAttributeDataProductFormNewAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAttributeDataProductFormNewAttributeActionGroup.xml new file mode 100644 index 0000000000000..560b3a7e9acef --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillAttributeDataProductFormNewAttributeActionGroup.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="AdminFillAttributeDataProductFormNewAttributeActionGroup"> + <arguments> + <argument name="attributeName" type="string" defaultValue="TestAttributeName"/> + <argument name="attributeType" type="string" defaultValue="Text Field"/> + </arguments> + <annotations> + <description>Fill attribute data on 'Create New Attribute' page Admin Product creation/edit page .</description> + </annotations> + <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" + stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" + stepKey="selectAttributeType"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 0bb73e7416b07..428b3828901cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -24,7 +24,7 @@ <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, product.type_id)}}" stepKey="seeNewProductUrl"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Product" stepKey="seeNewProductTitle"/> </actionGroup> - + <!--Navigate to create product page directly via ID--> <actionGroup name="goToProductPageViaID"> <annotations> @@ -108,6 +108,12 @@ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> </actionGroup> + <actionGroup name="AdminFillProductCountryOfManufactureActionGroup"> + <arguments> + <argument name="countryId" type="string" defaultValue="US"/> + </arguments> + <selectOption selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="{{countryId}}" stepKey="countryOfManufactureDropDown"/> + </actionGroup> <!--Check that required fields are actually required--> <actionGroup name="checkRequiredFieldsInProductForm"> @@ -134,8 +140,8 @@ <scrollToTopOfPage stepKey="scrollTopPageProduct"/> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitProductSaveSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> </actionGroup> <actionGroup name="toggleProductEnabled"> @@ -149,9 +155,10 @@ <!-- Save product but do not expect a success message --> <actionGroup name="SaveProductFormNoSuccessCheck" extends="saveProductForm"> <annotations> - <description>EXTENDS: saveProductForm. Removes 'seeSaveConfirmation'.</description> + <description>EXTENDS: saveProductForm. Removes 'waitProductSaveSuccessMessage' and 'seeSaveConfirmation'.</description> </annotations> + <remove keyForRemoval="waitProductSaveSuccessMessage"/> <remove keyForRemoval="seeSaveConfirmation"/> </actionGroup> @@ -183,6 +190,18 @@ <click selector="{{AdminProductImagesSection.removeImageButton}}" stepKey="clickRemoveImage"/> </actionGroup> + <!--Remove Product image by name--> + <actionGroup name="RemoveProductImageByName" extends="removeProductImage"> + <annotations> + <description>Removes a Product Image on the Admin Products creation/edit page by name.</description> + </annotations> + + <arguments> + <argument name="image" defaultValue="ProductImage"/> + </arguments> + <click selector="{{AdminProductImagesSection.removeImageButtonForExactImage(image.fileName)}}" stepKey="clickRemoveImage"/> + </actionGroup> + <!-- Assert product image in Admin Product page --> <actionGroup name="assertProductImageAdminProductPage"> <annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index 3e54574c553e3..42e0bb24c744f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -213,6 +213,7 @@ </arguments> <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAttributeGridPageLoad"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> @@ -319,6 +320,11 @@ <selectOption selector="{{AttributePropertiesSection.ValueRequired}}" stepKey="checkRequired" userInput="{{attribute.is_required_admin}}"/> <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> </actionGroup> + <actionGroup name="AdminCreateSearchableProductAttribute" extends="createProductAttribute" insertAfter="checkRequired"> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{StorefrontPropertiesSection.PageTitle}}" stepKey="waitTabLoad"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="setSearchable"/> + </actionGroup> <!-- Inputs text default value and attribute code--> <actionGroup name="createProductAttributeWithTextField" extends="createProductAttribute" insertAfter="checkRequired"> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml index 57b180ada1536..d20b44b0162f0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml @@ -23,7 +23,7 @@ <click selector="{{AdminUpdateAttributesSection.toggleDescription}}" stepKey="clickToChangeDescription"/> <fillField selector="{{AdminUpdateAttributesSection.description}}" userInput="{{product.description}}" stepKey="fillFieldDescription"/> <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="save"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitVisibleSuccessMessage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitVisibleSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml index e20b5f113a7ec..a7d5a3864c052 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -64,6 +64,16 @@ <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{label}}" stepKey="fillName"/> <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickSave1"/> </actionGroup> + <actionGroup name="AdminAddUnassignedAttributeToGroup" extends="CreateDefaultAttributeSet"> + <arguments> + <argument name="firstOption" type="string" defaultValue="color"/> + <argument name="secondOption" type="string" defaultValue="material"/> + <argument name="group" type="string" defaultValue="Product Details"/> + </arguments> + <dragAndDrop selector1="{{AdminProductAttributeSetSection.attribute(firstOption)}}" selector2="{{AdminProductAttributeSetSection.attribute(group)}}" stepKey="unassign1"/> + <dragAndDrop selector1="{{AdminProductAttributeSetSection.attribute(secondOption)}}" selector2="{{AdminProductAttributeSetSection.attribute(group)}}" stepKey="unassign2"/> + <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickSaveButton"/> + </actionGroup> <actionGroup name="goToAttributeGridPage"> <annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index 6a260bbf22522..320a322fc5f8e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -389,7 +389,7 @@ <waitForPageLoad stepKey="waitForGridLoad"/> </actionGroup> - <!--Filter and select the the product --> + <!--Filter and select the product --> <actionGroup name="filterAndSelectProduct"> <annotations> <description>Goes to the Admin Products grid. Filters the Product grid by the provided Product SKU.</description> @@ -423,4 +423,23 @@ <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> <waitForPageLoad stepKey="waitForGridLoad"/> </actionGroup> + + <actionGroup name="deleteAllProductsUsingProductGrid"> + <annotations> + <description>Deletes all products in Admin Products grid page.</description> + </annotations> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openAdminGridProductsPage"/> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clearGridFilters"/> + + <conditionalClick selector="{{AdminProductGridSection.multicheckDropdown}}" dependentSelector="{{AdminDataGridTableSection.dataGridEmpty}}" visible="false" stepKey="openMulticheckDropdown"/> + <conditionalClick selector="{{AdminProductGridSection.multicheckOption('Select All')}}" dependentSelector="{{AdminDataGridTableSection.dataGridEmpty}}" visible="false" stepKey="selectAllProductsInGrid"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + + <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitGridIsEmpty"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductStockStatusActionGroup.xml new file mode 100644 index 0000000000000..887845b1b51a5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductStockStatusActionGroup.xml @@ -0,0 +1,20 @@ +<?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="AssertAdminProductStockStatusActionGroup"> + <arguments> + <argument name="productId" type="string"/> + <argument name="stockStatus" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="goToProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <seeOptionIsSelected selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{stockStatus}}" + stepKey="checkProductStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml index 7fbe71cbee301..6f58299fe1446 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml @@ -13,12 +13,17 @@ <description>Goes to the Storefront. Validates that the 2 provided Products appear in the correct order.</description> </annotations> <arguments> + <argument name="page" defaultValue="{{StorefrontHomePage.url}}" type="string"/> <argument name="product_1"/> <argument name="product_2"/> </arguments> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <amOnPage url="{{page}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForPageLoad5" time="60"/> + <executeJS function="window.localStorage.clear()" stepKey="clearWidgetCache" /> + <reloadPage stepKey="goToHomePageAfterCacheCleared"/> + <waitForPageLoad stepKey="waitForPageLoad5AfterCacheCleared" time="60"/> + <waitForElement selector="{{StorefrontCategoryProductSection.ProductImageByNumber('1')}}" time="120" stepKey="waitCompareWidgetLoad" /> <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('1')}}" userInput="alt" stepKey="grabFirstProductName1_1"/> <assertEquals expected="{{product_1.name}}" actual="($grabFirstProductName1_1)" message="notExpectedOrder" stepKey="compare1"/> <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('2')}}" userInput="alt" stepKey="grabFirstProductName2_2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DisableProductLabelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DisableProductLabelActionGroup.xml new file mode 100644 index 0000000000000..a416957dabc2b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DisableProductLabelActionGroup.xml @@ -0,0 +1,27 @@ +<?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="DisableProductLabelActionGroup"> + <annotations> + <description>Disable Product Label and Change Attribute Set.</description> + </annotations> + <arguments> + <argument name="createAttributeSet"/> + </arguments> + + <checkOption selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad time="30" stepKey="waitForChangeAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{createAttributeSet.attribute_set_name}}" stepKey="searchForAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="dontSeeCheckboxEnableProductIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml index 2994533d79ed0..b8803fabda00d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml @@ -16,7 +16,7 @@ <argument name="productAttributeCode" type="string"/> </arguments> - <waitForElementVisible selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" stepKey="waitForAdminProductAttributeGridLoad"/> + <waitForElementVisible selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" before="seeAttributeCodeInGrid" stepKey="waitForAdminProductAttributeGridLoad"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" stepKey="clickAttributeToView"/> <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..4c641b621a504 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontAssertProductAbsentOnCategoryPageActionGroup"> + <annotations> + <description>Navigate to category page and verify product is absent.</description> + </annotations> + <arguments> + <argument name="category" defaultValue="_defaultCategory"/> + <argument name="product" defaultValue="SimpleProduct"/> + </arguments> + <amOnPage url="{{StorefrontCategoryPage.url(category.name)}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{product.name}}" stepKey="assertProductIsNotPresent"/> + <dontSee selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="{{product.price}}" stepKey="assertProductIsNotPricePresent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 341a00d3158d6..9393669f6e46d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -78,6 +78,15 @@ <seeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="AssertAddToCart"/> </actionGroup> + <actionGroup name="AssertProductOnCategoryPageActionGroup" extends="StorefrontCheckCategorySimpleProduct"> + <annotations> + <description>EXTENDS:StorefrontCheckCategorySimpleProduct. Removes 'AssertProductPrice', 'moveMouseOverProduct', 'AssertAddToCart'</description> + </annotations> + <remove keyForRemoval="AssertProductPrice"/> + <remove keyForRemoval="moveMouseOverProduct"/> + <remove keyForRemoval="AssertAddToCart"/> + </actionGroup> + <actionGroup name="StorefrontCheckAddToCartButtonAbsence"> <arguments> <argument name="product" defaultValue="_defaultProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml new file mode 100644 index 0000000000000..64dd2c97a382f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCategoryPageSortProductActionGroup"> + <annotations> + <description>Select "Sort by" parameter for sorting Products on Category page</description> + </annotations> + <arguments> + <argument name="sortBy" type="string" defaultValue="Price"/> + </arguments> + <selectOption selector="{{StorefrontCategoryTopToolbarSection.sortByDropdown}}" userInput="{{sortBy}}" stepKey="selectSortByParameter"/> + </actionGroup> + <actionGroup name="StorefrontCategoryPageSortAscendingActionGroup"> + <annotations> + <description>Set Ascending Direction for sorting Products on Category page</description> + </annotations> + <click selector="{{StorefrontCategoryTopToolbarSection.sortDirectionAsc}}" stepKey="setAscendingDirection"/> + </actionGroup> + <actionGroup name="StorefrontCategoryPageSortDescendingActionGroup"> + <annotations> + <description>Set Descending Direction for sorting Products on Category page</description> + </annotations> + <click selector="{{StorefrontCategoryTopToolbarSection.sortDirectionDesc}}" stepKey="setDescendingDirection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml index 899603aa27d75..e0229906ad558 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml @@ -18,4 +18,15 @@ <amOnPage url="{{StorefrontProductPage.url(productUrl)}}" stepKey="openProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoaded"/> </actionGroup> + <actionGroup name="StorefrontOpenProductPageOnSecondStore"> + <annotations> + <description>Goes to the Storefront Product page for the provided store code and Product URL.</description> + </annotations> + <arguments> + <argument name="storeCode" type="string"/> + <argument name="productUrl" type="string"/> + </arguments> + + <amOnPage url="{{StorefrontStoreViewProductPage.url(storeCode,productUrl)}}" stepKey="openProductPage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml similarity index 56% rename from app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml rename to app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml index 3a49b821ead5f..cb2bacfd2f2da 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml @@ -8,17 +8,18 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="EnableCatalogInventoryConfigData"> - <!--Default Value --> - <data key="path">cataloginventory/options/can_subtract</data> + <!-- Catalog > Price --> + <entity name="GlobalCatalogPriceScopeConfigData"> + <!-- Default configuration --> + <data key="path">catalog/price/scope</data> <data key="scope_id">0</data> - <data key="label">Yes</data> - <data key="value">1</data> + <data key="label">Global</data> + <data key="value">0</data> </entity> - <entity name="DisableCatalogInventoryConfigData"> - <data key="path">cataloginventory/options/can_subtract</data> + <entity name="WebsiteCatalogPriceScopeConfigData"> + <data key="path">catalog/price/scope</data> <data key="scope_id">0</data> - <data key="label">No</data> - <data key="value">0</data> + <data key="label">Website</data> + <data key="value">1</data> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceConfigData.xml new file mode 100644 index 0000000000000..50ce7f2da18c7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceConfigData.xml @@ -0,0 +1,19 @@ +<?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="CatalogPriceScopeWebsiteConfigData"> + <data key="path">catalog/price/scope</data> + <data key="value">1</data> + </entity> + <entity name="CatalogPriceScopeGlobalConfigData"> + <data key="path">catalog/price/scope</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index 13951a0d197d1..6ffb4e1902424 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -117,4 +117,14 @@ <data key="is_active">true</data> <data key="include_in_menu">true</data> </entity> + <entity name="DefaultCategory" type="category"> + <data key="name">Default Category</data> + </entity> + <!-- Category from file "export_import_configurable_product.csv" --> + <entity name="CategoryExportImport" extends="SimpleSubCategory" type="category"> + <data key="name">CategoryExportImport</data> + </entity> + <entity name="SubCategoryNonAnchor" extends="SubCategoryWithParent"> + <requiredEntity type="custom_attribute">CustomAttributeCategoryNonAnchor</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml index 1684bd0c8a2c3..1effb4ed0664e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml @@ -51,4 +51,24 @@ <data key="attribute_code">short_description</data> <data key="value">Short Fixedtest 555</data> </entity> + <entity name="CustomAttributeCategoryNonAnchor" type="custom_attribute"> + <data key="attribute_code">is_anchor</data> + <data key="value">0</data> + </entity> + <entity name="ProductDescriptionAdvancedSearchABC" type="custom_attribute"> + <data key="attribute_code">description</data> + <data key="value"><p>adc_Full</p></data> + </entity> + <entity name="ProductShortDescriptionAdvancedSearch" type="custom_attribute"> + <data key="attribute_code">short_description</data> + <data key="value"><p>abc_short</p></data> + </entity> + <entity name="ProductDescriptionAdvancedSearchADC123" type="custom_attribute"> + <data key="attribute_code">description</data> + <data key="value"><p>dfj_full</p></data> + </entity> + <entity name="ProductShortDescriptionAdvancedSearchADC123" type="custom_attribute"> + <data key="attribute_code">short_description</data> + <data key="value"><p>dfj_short</p></data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml index a2bdaa7dbc62f..e4ffdbde4368d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml @@ -20,4 +20,8 @@ <data key="store_id">0</data> <data key="label" unique="suffix">attributeThree</data> </entity> + <entity name="ProductAttributeFrontendLabelForExportImport" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label">attributeExportImport</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml index 1f4b1470098e2..6e40499d0efeb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml @@ -18,4 +18,7 @@ <data key="type">image/png</data> <data key="name" unique="prefix">magento-logo.png</data> </entity> + <entity name="MagentoLogoImageContentExportImport" extends="MagentoLogoImageContent" type="ImageContent"> + <data key="name">magento-logo.png</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 2deec6b8c1f8e..ca5c2f207fd35 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -108,6 +108,10 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="multipleSelectProductAttribute" extends="productDropDownAttribute" type="ProductAttribute"> + <data key="frontend_input">multiselect</data> + <data key="frontend_input_admin">Multiple Select</data> + </entity> <entity name="productDropDownAttributeNotSearchable" type="ProductAttribute"> <data key="attribute_code" unique="suffix">attribute</data> <data key="frontend_input">select</data> @@ -278,6 +282,28 @@ <data key="used_for_sort_by">false</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeTypeOfPrice" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">price</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_wysiwyg_enabled">false</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> <entity name="textProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> <data key="frontend_input">text</data> <data key="default_value" unique="suffix">defaultValue</data> @@ -376,4 +402,9 @@ <data key="frontend_label">Size</data> <data key="attribute_code" unique="suffix">size_attr</data> </entity> + <!-- Product attribute from file "export_import_configurable_product.csv" --> + <entity name="ProductAttributeWithTwoOptionsForExportImport" extends="productAttributeDropdownTwoOptions" type="ProductAttribute"> + <data key="attribute_code">attribute</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabelForExportImport</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml index 98c9a70e6aad4..75b4ef773a934 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml @@ -30,4 +30,9 @@ <data key="disabled">false</data> <requiredEntity type="ImageContent">MagentoLogoImageContent</requiredEntity> </entity> + <!-- From file "export_import_configurable_product.csv" --> + <entity name="ApiProductAttributeMediaGalleryForExportImport" extends="ApiProductAttributeMediaGalleryEntryTestImage" type="ProductAttributeMediaGalleryEntry"> + <data key="label">Magento Logo</data> + <requiredEntity type="ImageContent">MagentoLogoImageContentExportImport</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index fcb56cf298a98..a8646a58ae39c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -86,4 +86,19 @@ <data key="label" unique="suffix">White</data> <data key="value" unique="suffix">white</data> </entity> + <entity name="ProductAttributeOption9" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">Blue</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option11Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option11Store1</requiredEntity> + </entity> + <!-- Product attribute options from file "export_import_configurable_product.csv" --> + <entity name="ProductAttributeOptionOneForExportImport" extends="productAttributeOption1" type="ProductAttributeOption"> + <data key="label">option1</data> + </entity> + <entity name="ProductAttributeOptionTwoForExportImport" extends="productAttributeOption2" type="ProductAttributeOption"> + <data key="label">option2</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 517ab253b8238..bd02bc0e4b340 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -39,6 +39,10 @@ <data key="name">Pursuit Lumaflex&trade; Tone Band</data> <data key="sku" unique="suffix">x&trade;</data> </entity> + <entity name="SimpleProduct_25" type="product" extends="SimpleProduct2"> + <data key="quantity">25</data> + <requiredEntity type="product_extension_attribute">EavStock25</requiredEntity> + </entity> <entity name="ApiSimpleProductWithCustomPrice" type="product" extends="ApiSimpleProduct"> <data key="price">100</data> </entity> @@ -64,6 +68,23 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="ProductWithSpecialSymbols" extends="SimpleProduct" type="product"> + <data key="name">SimpleProduct -+~/\\<>\’“:*\$#@()!,.?`=%&^</data> + </entity> + <entity name="SimpleProductBeforeUpdate" type="product"> + <data key="sku">simpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">123.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">1000</data> + <data key="urlKey">simpleProduct</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="SimpleProductAfterImport1" type="product"> <data key="sku">SimpleProductForTest1</data> <data key="type_id">simple</data> @@ -351,6 +372,11 @@ <data key="filename">adobe-base</data> <data key="file_extension">jpg</data> </entity> + <entity name="TestImage" extends="TestImageAdobe" type="image"> + <data key="title" unique="suffix">test_image</data> + <data key="file">test_image.jpg</data> + <data key="filename">test_image</data> + </entity> <entity name="ProductWithUnicode" type="product"> <data key="sku" unique="suffix">霁产品</data> <data key="type_id">simple</data> @@ -433,6 +459,34 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiProductNameWithNoSpaces" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">ApiSimpleProduct</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> + <entity name="ApiProductWithDescriptionAndUnderscoredSku" type="product"> + <data key="sku" unique="suffix">api_simple_product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> <entity name="_newDefaultProduct" type="product"> <data key="sku" unique="suffix">testSku</data> <data key="type_id">simple</data> @@ -526,6 +580,20 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiVirtualProductWithDescriptionAndUnderscoredSku" type="product"> + <data key="sku" unique="suffix">api_virtual_product</data> + <data key="type_id">virtual</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Virtual Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-virtual-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> <entity name="SimpleProductWithNewFromDate" type="product"> <data key="sku" unique="suffix">SimpleProduct</data> <data key="type_id">simple</data> @@ -1021,6 +1089,10 @@ <entity name="productAlphabeticalB" type="product" extends="_defaultProduct"> <data key="name" unique="suffix">BBB Product</data> </entity> + <entity name="simpleProductWithShortNameAndSku" type="product" extends="defaultSimpleProduct"> + <data key="name">Test_Product</data> + <data key="sku">test_sku</data> + </entity> <entity name="productWithSpecialCharacters" type="product" extends="_defaultProduct"> <data key="name" unique="suffix">Product "!@#$%^&*()+:;\|}{][?=~` </data> <data key="nameWithSafeChars" unique="suffix">|}{][?=~` </data> @@ -1165,6 +1237,15 @@ <requiredEntity type="product_extension_attribute">EavStock10</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <!-- Products from file "export_import_configurable_product.csv" --> + <entity name="ApiSimpleOneExportImport" extends="ApiSimpleOne" type="product2"> + <data key="sku">api-simple-one-export-import</data> + <data key="name">Api Simple Product One Export Import</data> + </entity> + <entity name="ApiSimpleTwoExportImport" extends="ApiSimpleTwo" type="product2"> + <data key="sku">api-simple-two-export-import</data> + <data key="name">Api Simple Product Two Export Import</data> + </entity> <entity name="SimpleProductPrice10Qty1" type="product"> <data key="sku" unique="suffix">simple-product_</data> <data key="type_id">simple</data> @@ -1210,4 +1291,46 @@ <requiredEntity type="product_extension_attribute">EavStock1</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ABC_dfj_SimpleProduct" type="product"> + <data key="name" unique="suffix">abc_dfj_</data> + <data key="sku" unique="suffix">abc_dfj</data> + <data key="price">50.00</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductDescriptionAdvancedSearchABC</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductShortDescriptionAdvancedSearch</requiredEntity> + </entity> + <entity name="ABC_123_SimpleProduct" type="product"> + <data key="name" unique="suffix">adc_123_</data> + <data key="sku" unique="suffix">adc_123</data> + <data key="price">100.00</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductDescriptionAdvancedSearchADC123</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductShortDescriptionAdvancedSearchADC123</requiredEntity> + </entity> + <entity name="SimpleProductUpdatePrice11" type="product2"> + <data key="price">11.00</data> + </entity> + <entity name="SimpleProductUpdatePrice14" type="product2"> + <data key="price">14.00</data> + </entity> + <entity name="SimpleProductUpdatePrice16" type="product2"> + <data key="price">16.00</data> + </entity> + <entity name="ProductWithTwoTextFieldOptions" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionField</requiredEntity> + <requiredEntity type="product_option">ProductOptionField2</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml index 5a6a0b5dd9518..2576d8cdd7022 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml @@ -26,4 +26,7 @@ <entity name="EavStock777" type="product_extension_attribute"> <requiredEntity type="stock_item">Qty_777</requiredEntity> </entity> + <entity name="EavStock25" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_25</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductFormData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductFormData.xml new file mode 100644 index 0000000000000..933276edd834c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductFormData.xml @@ -0,0 +1,15 @@ +<?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="ProductFormMessages" type="message"> + <data key="remove_image_notice">The image cannot be removed as it has been assigned to the other image role</data> + <data key="save_success">You saved the product.</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml index 720087917aad4..404a4771a0e73 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml @@ -11,6 +11,7 @@ <entity name="ProductOptionField" type="product_option"> <var key="product_sku" entityType="product" entityKey="sku" /> <data key="title">OptionField</data> + <data key="sku">OptionField</data> <data key="type">field</data> <data key="is_require">true</data> <data key="sort_order">1</data> @@ -21,6 +22,7 @@ <entity name="ProductOptionField2" type="product_option"> <var key="product_sku" entityType="product" entityKey="sku" /> <data key="title">OptionField2</data> + <data key="sku">OptionField2</data> <data key="type">field</data> <data key="is_require">true</data> <data key="sort_order">1</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml index 32f4dc1404dd7..bef0b9f56eab6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml @@ -40,4 +40,8 @@ <data key="qty">777</data> <data key="is_in_stock">true</data> </entity> + <entity name="Qty_25" type="stock_item"> + <data key="qty">25</data> + <data key="is_in_stock">true</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml index 0e51995ac72e8..dcd7fde92283c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml @@ -72,4 +72,12 @@ <data key="store_id">1</data> <data key="label">Red</data> </entity> + <entity name="Option11Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">Blue</data> + </entity> + <entity name="Option11Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">Blue</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml index 18564ff101fd9..291ff115302f2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml @@ -32,4 +32,15 @@ <data key="display_on">All Pages</data> <data key="container">Sidebar Additional</data> </entity> + <entity name="CatalogCategoryLinkWidget" type="widget"> + <data key="type">Catalog Category Link</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Test Widget</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="sort_order">0</data> + <data key="display_on">All Pages</data> + <data key="container">Main Content Area</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml index e4c4ece5ac6cf..5c7cb4d51084f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml @@ -18,7 +18,6 @@ <section name="AdminProductAttributesSection"/> <section name="AdminProductFormRelatedUpSellCrossSellSection"/> <section name="AdminProductFormAdvancedPricingSection"/> - <section name="AdminProductFormAdvancedInventorySection"/> <section name="AdminAddAttributeModalSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml index 469c153d38b88..416e0d27a1824 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml @@ -8,7 +8,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="StorefrontCategoryPage" url="/{{var1}}.html" area="storefront" module="Catalog" parameterized="true"> + <page name="StorefrontCategoryPage" url="/{{var1}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> <section name="StorefrontCategoryMainSection"/> <section name="WYSIWYGToolbarSection"/> </page> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml new file mode 100644 index 0000000000000..046323bb368da --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.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"> + <!-- It is created to open product page with store code setting--> + <page name="StorefrontStoreViewProductPage" url="/{{storeCode}}/{{productUrlKey}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml new file mode 100644 index 0000000000000..d0200f1e0a5b0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.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="AdminCatalogStorefrontConfigSection"> + <element name="sectionHeader" type="button" selector="#catalog_frontend-head"/> + <element name="productsPerPageAllowedValues" type="input" selector="#catalog_frontend_grid_per_page_values"/> + <element name="productsPerPageDefaultValue" type="input" selector="#catalog_frontend_grid_per_page"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml index e3d224904671b..1cb095974d0fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml @@ -24,5 +24,6 @@ <element name="productTableColumnName" type="input" selector="#catalog_category_products_filter_name"/> <element name="productTableRow" type="button" selector="#catalog_category_products_table tbody tr"/> <element name="productSearch" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> + <element name="productTableColumnSku" type="input" selector="#catalog_category_products_filter_sku"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml index e8adede5b2de6..4aca4a09602b3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml @@ -13,6 +13,7 @@ <element name="DeleteButton" type="button" selector=".page-actions-inner #delete" timeout="30"/> <element name="CategoryStoreViewDropdownToggle" type="button" selector="#store-change-button"/> <element name="CategoryStoreViewOption" type="button" selector="//div[contains(@class, 'store-switcher')]//a[normalize-space()='{{store}}']" parameterized="true"/> + <element name="CategoryStoreViewOptionSelected" type="button" selector="//div[contains(@class, 'store-switcher')]//div[contains(@class,'actions')]//button[contains(text(),'{{store}}')]" parameterized="true"/> <element name="CategoryStoreViewModalAccept" type="button" selector=".modal-popup.confirm._show .action-accept"/> <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> <element name="storeSwitcher" type="text" selector=".store-switcher"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml index e9ff40f98bb16..8a993a74a58d1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml @@ -11,5 +11,6 @@ <section name="AdminCategoryProductsSection"> <element name="sectionHeader" type="button" selector="div[data-index='assign_products']" timeout="30"/> <element name="addProducts" type="button" selector="#catalog_category_add_product_tabs" timeout="30"/> + <element name="addProductsDisabled" type="button" selector="#catalog_category_add_product_tabs[disabled]" timeout="30"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index fba28b3feaff1..304c34b404ea5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -11,11 +11,15 @@ <section name="AdminCategorySidebarTreeSection"> <element name="collapseAll" type="button" selector=".tree-actions a:first-child"/> <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> + <element name="categoryHighlighted" type="text" selector="//div[@id='store.menu']//span[contains(text(),'{{name}}')]/ancestor::li" parameterized="true" timeout="30"/> + <element name="categoryNotHighlighted" type="text" selector="ul[id=\'ui-id-2\'] li[class~=\'active\']" timeout="30"/> <element name="categoryTreeRoot" type="text" selector="div.x-tree-root-node>li.x-tree-node:first-of-type>div.x-tree-node-el:first-of-type" timeout="30"/> <element name="categoryInTree" type="text" selector="//a/span[contains(text(), '{{name}}')]" parameterized="true" timeout="30"/> <element name="categoryInTreeUnderRoot" type="text" selector="//li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> <element name="lastCreatedCategory" type="block" selector=".x-tree-root-ct li li:last-child" /> <element name="treeContainer" type="block" selector=".tree-holder" /> <element name="expandRootCategory" type="text" selector="img.x-tree-elbow-end-plus"/> + <element name="expandRootCategoryByName" type="button" selector="//div[@class='x-tree-root-node']/li/div/a/span[contains(., '{{categoryName}}')]/../../img[contains(@class, 'x-tree-elbow-end-plus')]" parameterized="true" timeout="30"/> + <element name="categoryByName" type="text" selector="//div[contains(@class, 'categories-side-col')]//a/span[contains(text(), '{{categoryName}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml index 5329ad48c8f43..8bc9c03642c15 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewWidgetSection"> <element name="selectProduct" type="button" selector=".btn-chooser" timeout="30"/> + <element name="selectCategory" type="button" selector="button[title='Select Category...']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml index 6d4d5d86ef798..fa2f9feffdf91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml @@ -59,12 +59,12 @@ <element name="importOptions" type="button" selector="//button[@data-index='button_import']" timeout="30"/> </section> <section name="AdminProductImportOptionsSection"> - <element name="selectProductTitle" type="text" selector="//h1[contains(text(), 'Select Product')]" timeout="30"/> - <element name="filterButton" type="button" selector="//button[@data-action='grid-filter-expand']" timeout="30"/> - <element name="nameField" type="input" selector="//input[@name='name']" timeout="30"/> - <element name="applyFiltersButton" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> - <element name="resetFiltersButton" type="button" selector="//button[@data-action='grid-filter-reset']" timeout="30"/> - <element name="firstRowItemCheckbox" type="input" selector="//input[@data-action='select-row']" timeout="30"/> - <element name="importButton" type="button" selector="//button[contains(@class, 'action-primary')]/span[contains(text(), 'Import')]" timeout="30"/> + <element name="selectProductTitle" type="text" selector="//aside[contains(@class, 'product_form_product_form_import_options_modal')]//h1[contains(text(), 'Select Product')]" timeout="30"/> + <element name="filterButton" type="button" selector="aside.product_form_product_form_import_options_modal button[data-action='grid-filter-expand']" timeout="30"/> + <element name="nameField" type="input" selector="aside.product_form_product_form_import_options_modal input[name='name']" timeout="30"/> + <element name="applyFiltersButton" type="button" selector="aside.product_form_product_form_import_options_modal button[data-action='grid-filter-apply']" timeout="30"/> + <element name="resetFiltersButton" type="button" selector="aside.product_form_product_form_import_options_modal button[data-action='grid-filter-reset']" timeout="30"/> + <element name="firstRowItemCheckbox" type="input" selector="aside.product_form_product_form_import_options_modal input[data-action='select-row']" timeout="30"/> + <element name="importButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_import_options_modal')]//button[contains(@class, 'action-primary')]/span[contains(text(), 'Import')]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 80b4159167453..7649f6c344c3a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -8,6 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormSection"> + <element name="additionalOptions" type="select" selector=".admin__control-multiselect"/> + <element name="datepickerNewAttribute" type="input" selector="[data-index='{{attrName}}'] input" timeout="30" parameterized="true"/> <element name="attributeSet" type="select" selector="div[data-index='attribute_set_id'] .admin__field-control"/> <element name="attributeSetFilter" type="input" selector="div[data-index='attribute_set_id'] .admin__field-control input" timeout="30"/> <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> @@ -37,7 +39,7 @@ <element name="categoriesDropdown" type="multiselect" selector="div[data-index='category_ids']" timeout="30"/> <element name="unselectCategories" type="button" selector="//span[@class='admin__action-multiselect-crumb']/span[contains(.,'{{category}}')]/../button[@data-action='remove-selected-item']" parameterized="true" timeout="30"/> <element name="productQuantity" type="input" selector=".admin__field[data-index=qty] input"/> - <element name="advancedInventoryLink" type="button" selector="//button[contains(@data-index, 'advanced_inventory_button')]" timeout="30"/> + <element name="advancedInventoryLink" type="button" selector="button[data-index='advanced_inventory_button'].action-additional" timeout="30"/> <element name="productStockStatus" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]']" timeout="30"/> <element name="productStockStatusDisabled" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]'][disabled=true]"/> <element name="stockStatus" type="select" selector="[data-index='product-details'] select[name='product[quantity_and_stock_status][is_in_stock]']"/> @@ -156,6 +158,7 @@ <element name="FolderName" type="button" selector="input[data-role='promptField']" /> <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" timeout="30"/> <element name="StorageRootArrow" type="button" selector="#root > .jstree-icon" /> + <element name="FolderContainer" type="button" selector="div[data-role='tree']" /> <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> <element name="WysiwygArrow" type="button" selector="#d3lzaXd5Zw-- > .jstree-icon" /> <element name="checkIfWysiwygArrowExpand" type="button" selector="//li[@id='d3lzaXd5Zw--' and contains(@class,'jstree-closed')]" /> @@ -215,6 +218,7 @@ <element name="textAttributeByName" type="text" selector="//div[@data-index='attributes']//fieldset[contains(@class, 'admin__field') and .//*[contains(.,'{{var}}')]]//input" parameterized="true"/> <element name="dropDownAttribute" type="select" selector="//select[@name='product[{{arg}}]']" parameterized="true" timeout="30"/> <element name="attributeSection" type="block" selector="//div[@data-index='attributes']/div[contains(@class, 'admin__collapsible-content _show')]" timeout="30"/> + <element name="customAttribute" type="text" selector="product[{{attributecode}}]" timeout="30" parameterized="true"/> <element name="attributeGroupByName" type="button" selector="//div[@class='fieldset-wrapper-title']//span[text()='{{group}}']" parameterized="true"/> <element name="attributeByGroupAndName" type="text" selector="//div[@class='fieldset-wrapper-title']//span[text()='{{group}}']/../../following-sibling::div//span[contains(text(),'attribute')]" parameterized="true"/> </section> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml index 89eb1ed678cc9..f20e9b3a11e5e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml @@ -14,6 +14,7 @@ <element name="imageUploadButton" type="button" selector="div.image div.fileinput-button"/> <element name="imageFile" type="text" selector="//*[@id='media_gallery_content']//img[contains(@src, '{{url}}')]" parameterized="true"/> <element name="removeImageButton" type="button" selector=".action-remove"/> + <element name="removeImageButtonForExactImage" type="button" selector="[id='media_gallery_content'] img[src*='{{imageName}}'] + div[class='actions'] button[class='action-remove']" parameterized="true"/> <element name="modalOkBtn" type="button" selector="button.action-primary.action-accept"/> <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> <element name="productImagesToggleState" type="button" selector="[data-index='gallery'] > [data-state-collapsible='{{status}}']" parameterized="true"/> @@ -27,6 +28,7 @@ <element name="roleSmall" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Small']"/> <element name="roleThumbnail" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Thumbnail']"/> <element name="roleSwatch" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Swatch']"/> + <element name="isRoleChecked" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = '{{role}}']/parent::li[contains(@class,'selected')]" parameterized="true"/> <element name="isBaseSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Base']"/> <element name="isSmallSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Small']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml index ddec4428f90e2..faee605e77319 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontCategoryFilterSection"> <element name="CategoryFilter" type="button" selector="//main//div[@class='filter-options']//div[contains(text(), 'Category')]"/> <element name="CategoryByName" type="button" selector="//main//div[@class='filter-options']//li[@class='item']//a[contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="CustomPriceAttribute" type="button" selector="div.filter-options-title"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index ac7a15daf56aa..9a84f90edcfc0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -34,5 +34,6 @@ <element name="productsList" type="block" selector="#maincontent .column.main"/> <element name="productName" type="text" selector=".product-item-name"/> <element name="productOptionList" type="text" selector="#narrow-by-list"/> + <element name="productNameByPosition" type="text" selector=".products-grid li:nth-of-type({{position}}) .product-item-name a" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml index 1b7bbd58eea9f..2ec7997f87b7e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml @@ -8,10 +8,13 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCategorySidebarSection"> + <element name="layeredFilterBlock" type="block" selector="#layered-filter-block"/> <element name="filterOptionsTitle" type="text" selector="//div[@class='filter-options-title' and contains(text(), '{{var1}}')]" parameterized="true"/> <element name="filterOptions" type="text" selector=".filter-options-content .items"/> <element name="filterOption" type="text" selector=".filter-options-content .item"/> <element name="optionQty" type="text" selector=".filter-options-content .item .count"/> + <element name="filterOptionByLabel" type="button" selector=" div.filter-options-item div[option-label='{{optionLabel}}']" parameterized="true"/> + <element name="removeFilter" type="button" selector="div.filter-current .remove"/> </section> <section name="StorefrontCategorySidebarMobileSection"> <element name="shopByButton" type="button" selector="//div[contains(@class, 'filter-title')]/strong[contains(text(), 'Shop By')]"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml index 87aab45bd8cb7..ca0c32f142852 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml @@ -12,5 +12,6 @@ <element name="widgetRecentlyViewedProductsGrid" type="block" selector=".block.widget.block-viewed-products-grid"/> <element name="widgetRecentlyComparedProductsGrid" type="block" selector=".block.widget.block-compared-products-grid"/> <element name="widgetRecentlyOrderedProductsGrid" type="block" selector=".block.block-reorder"/> + <element name="widgetCategoryLinkByName" type="text" selector="//div[contains(@class, 'block-category-link')]/a/span[contains(., '{{categoryName}}')]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml index 31204c7b4b0bf..9a0f5ad002725 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -18,13 +18,10 @@ <testCaseId value="MAGETWO-98644"/> <useCaseId value="MAGETWO-98522"/> <group value="Catalog"/> - <skip> - <issueId value="MC-15930"/> - </skip> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo"/> + <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> <createData entity="SimpleSubCategory" stepKey="category"/> <createData entity="SimpleProduct4" stepKey="product"> @@ -32,7 +29,7 @@ </createData> </before> <after> - <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo2"/> + <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> @@ -40,23 +37,24 @@ </after> <!--Open product page--> <comment userInput="Open product page" stepKey="openProdPage"/> - <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage"/> + <amOnPage url="{{StorefrontProductPage.url($$product.custom_attributes[url_key]$$)}}" stepKey="goToSimpleProductPage"/> <waitForPageLoad stepKey="waitForSimpleProductPage"/> <!--'Add to compare' link is not available--> <comment userInput="'Add to compare' link is not available" stepKey="addToCompareLinkAvailability"/> <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="dontSeeAddToCompareLink"/> <!--Turn on 'out on stock' config--> <comment userInput="Turn on 'out of stock' config" stepKey="onOutOfStockConfig"/> - <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="displayOutOfStockYes"/> + <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockEnable.path}} {{CatalogInventoryOptionsShowOutOfStockEnable.value}}" stepKey="setConfigShowOutOfStockTrue"/> <!--Clear cache and reindex--> <comment userInput="Clear cache and reindex" stepKey="cleanCache"/> <magentoCLI command="indexer:reindex" stepKey="reindex"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Open product page--> <comment userInput="Open product page" stepKey="openProductPage"/> - <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage2"/> + <amOnPage url="{{StorefrontProductPage.url($$product.custom_attributes[url_key]$$)}}" stepKey="goToSimpleProductPage2"/> <waitForPageLoad stepKey="waitForSimpleProductPage2"/> <!--Click on 'Add to Compare' link--> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="seeAddToCompareLink"/> <comment userInput="Click on 'Add to Compare' link" stepKey="clickOnAddToCompareLink"/> <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickOnAddToCompare"/> <waitForPageLoad stepKey="waitForProdAddToCmpList"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml index 50d192a27e46d..c36c29ce594d0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml @@ -51,6 +51,9 @@ <seeElement selector="{{StorefrontCategoryMainSection.mediaDescription(ImageUpload3.content)}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> <after> + <actionGroup ref="DeleteCategory" stepKey="DeleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index f3d3e653b260b..4044490c92334 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -16,9 +16,6 @@ <description value="Admin should be able to add image to WYSIWYG Editor on Product Page"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84375"/> - <skip> - <issueId value="MC-17232"/> - </skip> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="login"/> @@ -73,11 +70,14 @@ <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="scrollToTinyMCE4" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse2" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoading13"/> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.CancelBtn}}" stepKey="waitForCancelButton2"/> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.CancelBtn}}" userInput="Cancel" stepKey="seeCancelBtn2" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading13"/> + <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" stepKey="waitForCreateFolderBtn2"/> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.CreateFolder}}" userInput="Create Folder" stepKey="seeCreateFolderBtn2" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading14"/> + <see selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderContainer}}" userInput="Storage Root" stepKey="seeFolderContainer" /> + <click userInput="Storage Root" stepKey="clickOnRootFolder" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoading15"/> <dontSeeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn3" /> <attachFile selector="{{ProductShortDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload3.value}}" stepKey="uploadImage3"/> <waitForLoadingMaskToDisappear stepKey="waitForFileUpload3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index feb4fffd12f5d..51ef7fb77d74c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -60,6 +60,9 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Clear cache and reindex--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Verify product is visible in category front page --> <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index e1cb45be22b4e..a863de2716c97 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -63,6 +63,10 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify product is visible in category front page --> <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml index f40a62c164ecc..1b72458747067 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -41,6 +41,16 @@ <createData entity="PaginationProduct" stepKey="simpleProduct18"/> <createData entity="PaginationProduct" stepKey="simpleProduct19"/> <createData entity="PaginationProduct" stepKey="simpleProduct20"/> + <createData entity="PaginationProduct" stepKey="simpleProduct21"/> + <createData entity="PaginationProduct" stepKey="simpleProduct22"/> + <createData entity="PaginationProduct" stepKey="simpleProduct23"/> + <createData entity="PaginationProduct" stepKey="simpleProduct24"/> + <createData entity="PaginationProduct" stepKey="simpleProduct25"/> + <createData entity="PaginationProduct" stepKey="simpleProduct26"/> + <createData entity="PaginationProduct" stepKey="simpleProduct27"/> + <createData entity="PaginationProduct" stepKey="simpleProduct28"/> + <createData entity="PaginationProduct" stepKey="simpleProduct29"/> + <createData entity="PaginationProduct" stepKey="simpleProduct30"/> <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> </before> <after> @@ -67,18 +77,36 @@ <deleteData createDataKey="simpleProduct18" stepKey="deleteSimpleProduct18"/> <deleteData createDataKey="simpleProduct19" stepKey="deleteSimpleProduct19"/> <deleteData createDataKey="simpleProduct20" stepKey="deleteSimpleProduct20"/> + <deleteData createDataKey="simpleProduct21" stepKey="deleteSimpleProduct21"/> + <deleteData createDataKey="simpleProduct22" stepKey="deleteSimpleProduct22"/> + <deleteData createDataKey="simpleProduct23" stepKey="deleteSimpleProduct23"/> + <deleteData createDataKey="simpleProduct24" stepKey="deleteSimpleProduct24"/> + <deleteData createDataKey="simpleProduct25" stepKey="deleteSimpleProduct25"/> + <deleteData createDataKey="simpleProduct26" stepKey="deleteSimpleProduct26"/> + <deleteData createDataKey="simpleProduct27" stepKey="deleteSimpleProduct27"/> + <deleteData createDataKey="simpleProduct28" stepKey="deleteSimpleProduct28"/> + <deleteData createDataKey="simpleProduct29" stepKey="deleteSimpleProduct29"/> + <deleteData createDataKey="simpleProduct30" stepKey="deleteSimpleProduct30"/> <actionGroup ref="logout" stepKey="logout"/> </after> - + <!--Verify default number of products displayed in the grid view--> + <comment userInput="Verify default number of products displayed in the grid view" stepKey="commentVerifyDefaultValues"/> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigPagePage"/> + <waitForPageLoad stepKey="waitForConfigPageLoad" /> + <conditionalClick selector="{{AdminCatalogStorefrontConfigSection.sectionHeader}}" dependentSelector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" visible="false" stepKey="openCatalogConfigStorefrontSection"/> + <waitForElementVisible selector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" stepKey="waitForSectionOpen"/> + <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" userInput="12,24,36" stepKey="seeDefaultValueAllowedNumberProductsPerPage"/> + <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageDefaultValue}}" userInput="12" stepKey="seeDefaultValueProductPerPage"/> <!--Open Category Page and select created category--> + <comment userInput="Open Category Page and select created category" stepKey="commentOpenCategoryPage"/> <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> <waitForPageLoad stepKey="waitForPageToLoad1"/> <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> <waitForPageLoad stepKey="waitForPageToLoad0"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForPageToLoaded2"/> - <!--Select Products--> + <comment userInput="Select Products" stepKey="commentSelectProducts"/> <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> <waitForPageLoad stepKey="waitForProductsToLoad"/> @@ -86,91 +114,92 @@ <waitForElementVisible selector="{{CatalogProductsSection.resetFilter}}" time="30" stepKey="waitForResetButtonToVisible"/> <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> <waitForPageLoad stepKey="waitForPageToLoad3"/> - <selectOption selector="{{AdminProductGridFilterSection.productPerPage}}" userInput="20" stepKey="selectPagePerView"/> + <selectOption selector="{{AdminProductGridFilterSection.productPerPage}}" userInput="30" stepKey="selectPagePerView"/> + <wait stepKey="waitFroPageToLoad1" time="30"/> <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="pagi" stepKey="selectProduct1"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitFroPageToLoad1"/> - <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="20" stepKey="seeNumberOfProductsFound"/> + <waitForPageLoad stepKey="waitFroPageToLoad2"/> + <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="30" stepKey="seeNumberOfProductsFound"/> <click selector="{{AdminCategoryProductsGridSection.productSelectAll}}" stepKey="selectSelectAll"/> <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> <waitForPageLoad stepKey="waitForCategorySaved"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> - <!--Open Category Store Front Page--> + <comment userInput="Open Category Store Front Page" stepKey="commentOpenCategoryOnStorefront"/> <amOnPage url="{{_defaultCategory.name}}.html" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForProductToLoad"/> - - <!--Select 9 items per page and verify number of products displayed in each page --> + <!--Select 12 items per page and verify number of products displayed in each page --> + <comment userInput="Select 12 items per page and verify number of products displayed in each page" stepKey="comment12ItemsPerPage"/> <conditionalClick selector="{{StorefrontCategoryTopToolbarSection.gridMode}}" visible="true" dependentSelector="{{StorefrontCategoryTopToolbarSection.gridMode}}" stepKey="seeProductGridIsActive"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToBottomToolbarSection"/> - <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="9" stepKey="selectPerPageOption"/> - + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="12" stepKey="selectPerPageOption"/> <!--Verify number of products displayed in First Page --> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage"/> - + <comment userInput="Verify number of products displayed in First Page" stepKey="commentVerifyNumberOfProducts"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="12" stepKey="seeNumberOfProductsInFirstPage"/> <!--Verify number of products displayed in Second Page --> + <comment userInput="Verify number of products displayed in Second Page" stepKey="commentVerifyNumberOfProductsSecondPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> <waitForPageLoad stepKey="waitForPageToLoad4"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage"/> - + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="12" stepKey="seeNumberOfProductsInSecondPage"/> <!--Verify number of products displayed in third Page --> + <comment userInput="Verify number of products displayed in third Page" stepKey="commentVerifyNumberOfProductsThirdPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton1"/> <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage1"/> <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage"/> - + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="6" stepKey="seeNumberOfProductsInThirdPage"/> <!--Change Pages using Previous Page selector and verify number of products displayed in each page--> + <comment userInput="Change Pages using Previous Page selector and verify number of products displayed in each page" stepKey="commentVerifyProductsOnEachPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage"/> <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage1"/> <waitForPageLoad stepKey="waitForPageToLoad5"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage1"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="12" stepKey="seeNumberOfProductsInSecondPage1"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage1"/> <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage2"/> <waitForPageLoad stepKey="waitForPageToLoad6"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage1"/> - + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="12" stepKey="seeNumberOfProductsInFirstPage1"/> <!--Select Pages by using page Number and verify number of products displayed--> + <comment userInput="Select Pages by using page Number and verify number of products displayed" stepKey="commentSelectPagesAndVerify"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage2"/> <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('2')}}" stepKey="clickOnPage2"/> <waitForPageLoad stepKey="waitForPageToLoad7"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage2"/> - + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="12" stepKey="seeNumberOfProductsInSecondPage2"/> <!--Select Third Page using page number--> + <comment userInput="Select Third Page using page number" stepKey="commentSelectThirdPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage3"/> <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('3')}}" stepKey="clickOnThirdPage"/> <waitForPageLoad stepKey="waitForPageToLoad8"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage2"/> - + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="6" stepKey="seeNumberOfProductsInThirdPage2"/> <!--Select First Page using page number--> + <comment userInput="Select First Page using page number" stepKey="commentSelectFirstPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage4"/> <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage"/> <waitForPageLoad stepKey="waitForPageToLoad9"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsFirstPage2"/> - - <!--Select 15 items per page and verify number of products displayed in each page --> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="12" stepKey="seeNumberOfProductsFirstPage2"/> + <!--Select 24 items per page and verify number of products displayed in each page --> + <comment userInput="Select 24 items per page and verify number of products displayed in each page" stepKey="commentSelect24ItemsPerPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage"/> - <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="15" stepKey="selectPerPageOption1"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="24" stepKey="selectPerPageOption1"/> <waitForPageLoad stepKey="waitForPageToLoad10"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="15" stepKey="seeNumberOfProductsInFirstPage3"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="24" stepKey="seeNumberOfProductsInFirstPage3"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton2"/> <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage2"/> <waitForPageLoad stepKey="waitForPageToLoad11"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="5" stepKey="seeNumberOfProductsInSecondPage3"/> - + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="6" stepKey="seeNumberOfProductsInSecondPage3"/> <!--Select First Page using page number--> + <comment userInput="Select First Page using page number" stepKey="commentSelectFirstPageSecondTime"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="scrollToPreviousPage5"/> <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage2"/> <waitForPageLoad stepKey="waitForPageToLoad13"/> - - <!--Select 30 items per page and verify number of products displayed in each page --> + <!--Select 36 items per page and verify number of products displayed in each page --> + <comment userInput="Select 36 items per page and verify number of products displayed in each page" stepKey="commentSelect36ItemsPerPage"/> <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage4"/> - <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="30" stepKey="selectPerPageOption2"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="36" stepKey="selectPerPageOption2"/> <waitForPageLoad stepKey="waitForPageToLoad12"/> - <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="20" stepKey="seeNumberOfProductsInFirstPage4"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="30" stepKey="seeNumberOfProductsInFirstPage4"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml index 15171fe3713c3..784b5d3fd1827 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml @@ -19,7 +19,7 @@ <group value="category"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index 9115004ad9585..a6890c2ad4905 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -64,6 +64,9 @@ <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> <!--Verify the Category Title--> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <!--Clear cache and reindex--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Verify Product in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name_lwr)}}" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml new file mode 100644 index 0000000000000..2706d00038e4b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml @@ -0,0 +1,51 @@ +<?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="AdminCreateNewAttributeFromProductPageTest"> + <annotations> + <features value="Catalog"/> + <stories value="We should validate the form when the user click Save in New Attribute Set"/> + <title value="We should validate the form when the user click Save in New Attribute Set"/> + <description + value="Admin should be able to create product attribute and validate the form when the user click Save in New Attribute Set"/> + <testCaseId value="https://github.com/magento/magento2/pull/25132"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="AdminClickAddAttributeOnProductEditPageActionGroup" stepKey="clickAddAttribute"/> + <actionGroup ref="AdminClickCreateNewAttributeFromProductEditPageActionGroup" stepKey="clickCreateNewAttribute1" /> + <actionGroup ref="AdminFillAttributeDataProductFormNewAttributeActionGroup" stepKey="fillAttributeData" /> + + <click selector="{{AdminProductFormNewAttributeSection.saveInNewSet}}" stepKey="saveAttributeInSet"/> + <actionGroup ref="AdminAssertProductEditPageCreateAttributeSaveInAttributeSetPopUpShownActionGroup" stepKey="assertPopUp"/> + <click selector="{{ModalConfirmationSection.CancelButton}}" stepKey="cancelButton"/> + + <actionGroup ref="AdminFillAttributeDataProductFormNewAttributeActionGroup" stepKey="emptyAttributeData" > + <argument name="attributeName" value=" "/> + <argument name="attributeType" value=" "/> + </actionGroup> + + <click selector="{{AdminProductFormNewAttributeSection.saveInNewSet}}" stepKey="clickSaveInSet"/> + <see userInput="This is a required field." stepKey="seeThisIsRequiredField"/> + <dontSee userInput="Enter Name for New Attribute Set" stepKey="dontSeePopUp" /> + + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 5c798db29b976..63a964f4b5e91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -90,6 +90,9 @@ <waitForPageLoad stepKey="waitForProductToSave"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Run Re-Index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify product attribute added in product form --> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml index 6658ad36d7150..291b6985bd3e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml @@ -23,7 +23,7 @@ </createData> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml index 6096ee1fa3996..a7587a5ed31fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml @@ -22,7 +22,6 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 896a28d0298e6..94d488f216b49 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 58737dd509743..23f772a395a7d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -117,6 +117,8 @@ <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml index 78247f4943596..9055e961f889f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -24,6 +24,9 @@ <createData entity="Simple_US_CA_Customer" stepKey="customer" /> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="virtualProductGeneralGroup"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index 6ef2569945fa6..40f26761e7b6d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -97,6 +97,10 @@ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Verify customer see created virtual product with tier price(from above step) on storefront page and is searchable by sku --> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml new file mode 100644 index 0000000000000..f334cbc218b7c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -0,0 +1,186 @@ +<?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="AdminDeleteProductsImageInCaseOfMultipleStoresTest"> + <annotations> + <stories value="MultipleStores"/> + <features value="Catalog"/> + <title value="Delete products image in case of multiple stores"/> + <description value="Delete products image in case of multiple stores"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11466"/> + <useCaseId value="MC-15391"/> + <group value="Catalog"/> + </annotations> + <before> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create new website, store and store view--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <!--Create Product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <createData entity="SubCategory" stepKey="createSubCategory"/> + <createData entity="NewRootCategory" stepKey="createRootCategory"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad0"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="['Default Category', $$createRootCategory.name$$, $$createSubCategory.name$$]" stepKey="fillCategory"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Add images to the product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="visitAdminProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <actionGroup ref="addProductImage" stepKey="addImageToProduct"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="addProductImage" stepKey="addImage1ToProduct"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + <!--Enable config to view created store view on store front--> + <createData entity="EnableWebUrlOptionsConfig" stepKey="enableWebUrlOptionsConfig"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="createRootCategory" stepKey="deleteRootCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="DefaultWebUrlOptionsConfig" stepKey="defaultWebUrlOptionsConfig"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Grab new store view code--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToNewWebsitePage"/> + <waitForPageLoad stepKey="waitForStoresPageLoad"/> + <fillField userInput="{{NewWebSiteData.name}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickFirstRow"/> + <grabValueFrom selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="grabStoreViewCode"/> + <click selector="{{AdminNewStoreViewActionsSection.backButton}}" stepKey="clickBack"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickResetButton"/> + <waitForPageLoad stepKey="waitForStorePageLoad"/> + <!--Open product page on admin--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <!--Enable the newly created website and save the product--> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectWebsiteInProduct2"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2"/> + <!--Reindex and flush cache--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Switch to 'Default Store View' scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchDefaultStoreView"> + <argument name="storeViewName" value="'Default Store View'"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad3"/> + <!--Assign all roles to first image on default store view--> + <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToFirstImage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct3"/> + <!--Switch to newly created Store View scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchNewStoreView"> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad4"/> + <!--Assign all roles to first image on new store view--> + <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToFirstImage2"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct4"/> + <!--Switch to 'All Store Views' scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchAllStoreView"> + <argument name="storeViewName" value="'All Store Views'"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad5"/> + <!--Remove product image and save--> + <actionGroup ref="RemoveProductImageByName" stepKey="removeProductImage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct5"/> + <!--Assert notification and success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification"/> + <!--Reopen image tab and see the image is not deleted--> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab"/> + <waitForPageLoad stepKey="waitForImagesLoad"/> + <seeElement selector="{{AdminProductImagesSection.imageFile(ProductImage.fileName)}}" stepKey="seeImageIsNotDeleted"/> + <!--Switch to newly created Store View scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchNewStoreView2"> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad6"/> + <!--Assign all roles to second image on default store view--> + <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToSecondImage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct6"/> + <!--Switch to 'All Store Views' scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchAllStoreView2"> + <argument name="storeViewName" value="'All Store Views'"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad7"/> + <!--Remove product image and save--> + <actionGroup ref="RemoveProductImageByName" stepKey="removeProductFirstImage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct7"/> + <!--Assert notification and success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage2"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification2"/> + <!--Reopen image tab and see the image is not deleted--> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab2"/> + <waitForPageLoad stepKey="waitForImagesLoad2"/> + <seeElement selector="{{AdminProductImagesSection.imageFile(ProductImage.fileName)}}" stepKey="seeImageIsNotDeleted2"/> + <!--Switch to newly created Store View scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchNewStoreView3"> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad8"/> + <!--Remove second image and save--> + <actionGroup ref="RemoveProductImageByName" stepKey="removeProductSecondImage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct8"/> + <!--Assert success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage3"/> + <!--Reopen image tab and see the image is deleted--> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab3"/> + <waitForPageLoad stepKey="waitForImagesLoad3"/> + <dontSeeElement selector="{{AdminProductImagesSection.imageFile(TestImageNew.fileName)}}" stepKey="seeImageIsDeleted"/> + <!--Open Storefront on Default store view and assert image existence--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad0"/> + <grabAttributeFrom userInput="src" selector="{{StorefrontCategoryMainSection.mediaDescription($$createProduct.name$$)}}" stepKey="grabAttributeFromImage"/> + <assertContains expectedType="string" expected="{{ProductImage.filename}}" actual="$grabAttributeFromImage" stepKey="assertProductImageAbsence"/> + <!--Open Storefront on newly created store view and assert image absence--> + <amOnPage url="$grabStoreViewCode" stepKey="navigateToHomePageOfSpecificStore"/> + <waitForPageLoad stepKey="waitForHomePageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSubCategory.name$$)}}" stepKey="clickCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad1"/> + <grabAttributeFrom userInput="src" selector="{{StorefrontCategoryMainSection.mediaDescription($$createProduct.name$$)}}" stepKey="grabAttributeFromImage2"/> + <assertContains expectedType="string" expected="small_image" actual="$grabAttributeFromImage2" stepKey="assertProductImageAbsence2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml new file mode 100644 index 0000000000000..dab1704d50bf3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml @@ -0,0 +1,49 @@ +<?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="AdminDisableProductOnChangingAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <stories value="Disabled product is enabled when change attribute set"/> + <title value="Verify product status while changing attribute set"/> + <description value="Value set for enabled product has to be shown when attribute set is changed"/> + <severity value="MAJOR"/> + <testCaseId value="MC-19716"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$$createAttributeSet.attribute_set_id$$/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="DisableProductLabelActionGroup" stepKey="disableWhileChangingAttributeSet" > + <argument name="createAttributeSet" value="$$createAttributeSet$$"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml index 5c434ecabf80d..41b446b474078 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -131,7 +131,7 @@ userInput="$$createProduct1.name$$" stepKey="seeProductName4"/> <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" userInput="$$createProduct12.name$$" stepKey="seeProductName5"/> - <waitForText userInput="$$createCategory.name$$ (2)" stepKey="seeCorrectProductCount"/> + <waitForText userInput="$$createCategory.name$$ (ID: 6) (2)" stepKey="seeCorrectProductCount"/> <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" userInput="$$createProduct0.name$$" stepKey="dontSeeProductName"/> <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" @@ -151,7 +151,7 @@ userInput="$$createProduct2.name$$" stepKey="seeProductName6"/> <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" userInput="$$createProduct12.name$$" stepKey="seeProductName7"/> - <waitForText userInput="$$createCategory.name$$ (2)" stepKey="seeCorrectProductCount2"/> + <waitForText userInput="$$createCategory.name$$ (ID: 6) (2)" stepKey="seeCorrectProductCount2"/> <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" userInput="$$createProduct0.name$$" stepKey="dontSeeProductName2"/> <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml index df6ddfa169029..eb4a561760070 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -25,9 +25,7 @@ <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> <waitForPageLoad stepKey="waitForProductIndexPage"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" - dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> - <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridToDefaultView"/> <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteProductIfTheyExist"/> <createData stepKey="category1" entity="SimpleSubCategory"/> <createData stepKey="product1" entity="SimpleProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index b1f00a2f51a95..dfadfdf00481b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -15,55 +15,44 @@ <title value="Import customizable options to a product with existing SKU"/> <description value="Import customizable options to a product with existing SKU"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-98211"/> + <testCaseId value="MC-16471"/> <useCaseId value="MAGETWO-70232"/> <group value="catalog"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <!--Create category--> - <comment userInput="Create category" stepKey="commentCreateCategory"/> <createData entity="ApiCategory" stepKey="createCategory"/> - <!-- Create two product --> - <comment userInput="Create two product" stepKey="commentCreateTwoProduct"/> + <!-- Create two products --> <createData entity="SimpleProduct2" stepKey="createFirstProduct"/> + <updateData entity="ProductWithTwoTextFieldOptions" createDataKey="createFirstProduct" stepKey="updateFirstProductWithCustomOptions"> + <requiredEntity createDataKey="createFirstProduct"/> + </updateData> <createData entity="ApiSimpleProduct" stepKey="createSecondProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> <!--Delete second product with changed sku--> - <comment userInput="Delete second product with changed sku" stepKey="commentDeleteProduct"/> <actionGroup ref="deleteProductBySku" stepKey="deleteSecondProduct"> <argument name="sku" value="$$createFirstProduct.sku$$-1"/> </actionGroup> - <!--Delete created data--> - <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductGridFilter"/> <actionGroup ref="logout" stepKey="logoutOfAdmin"/> </after> - <!--Go to product page --> - <comment userInput="Go to product page" stepKey="commentGoToProductPage"/> - <amOnPage url="{{AdminProductEditPage.url($$createFirstProduct.id$$)}}" stepKey="goToProductEditPage"/> - <waitForPageLoad stepKey="waitForProductEditPageLoad"/> - <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption1"> - <argument name="option" value="ProductOptionField"/> - </actionGroup> - <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption2"> - <argument name="option" value="ProductOptionField2"/> - </actionGroup> - <actionGroup ref="saveProductForm" stepKey="saveProduct"/> <!--Change second product sku to first product sku--> - <comment userInput="Change second product sku to first product sku" stepKey="commentChangeSecondProduct"/> <amOnPage url="{{AdminProductEditPage.url($$createSecondProduct.id$$)}}" stepKey="goToProductEditPage1"/> <waitForPageLoad stepKey="waitForProductEditPageLoad1"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="$$createFirstProduct.sku$$" stepKey="fillProductSku1"/> <!--Import customizable options and check--> - <comment userInput="Import customizable options and check" stepKey="commentImportOptions"/> <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> <actionGroup ref="importProductCustomizableOptions" stepKey="importOptions"> <argument name="productName" value="$$createFirstProduct.name$$"/> @@ -77,8 +66,7 @@ <argument name="optionIndex" value="1"/> </actionGroup> <!--Save product and check sku changed message--> - <comment userInput="Save product and check sku changed message" stepKey="commentSAveProductAndCheck"/> - <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> - <see userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> + <actionGroup ref="saveProductForm" stepKey="saveSecondProduct"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index 4d581bae700d7..f5ad5b8079d1f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMassProductPriceUpdateTest"> <annotations> - <stories value="Mass product update "/> + <stories value="Mass product update"/> <features value="Catalog"/> <title value="Mass update simple product price"/> <description value="Login as admin and update mass product price"/> @@ -24,8 +24,8 @@ <createData entity="defaultSimpleProduct" stepKey="simpleProduct2"/> </before> <after> - <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> - <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index 87e0bf3d2e9a0..989431941b279 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -16,14 +16,12 @@ <description value="Admin should be able to mass update product attributes in global scope"/> <severity value="AVERAGE"/> <testCaseId value="MC-56"/> - <group value="Catalog"/> - <group value="Product Attributes"/> - <skip> - <issueId value="MC-17140"/> - </skip> + <group value="catalog"/> + <group value="product_attributes"/> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="ApiSimpleProduct" stepKey="createProductOne"> @@ -38,7 +36,9 @@ <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="AdminDeleteStoreViewActionGroup"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> </after> <!-- Search and select products --> @@ -58,36 +58,38 @@ <!-- Switch store view --> <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchStoreViewActionGroup"/> <!-- Update attribute --> - <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> + <checkOption selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$$createProductOne.price$$0" stepKey="fillAttributeNameField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" time="60" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeAttributeUpdateSuccessMsg"/> <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron1"/> + <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="searchByNameDefault"> - <argument name="name" value="$$createProductOne.name$$"/> + <argument name="name" value=""$$createProductOne.name$$""/> <argument name="priceFrom" value="$$createProductOne.price$$0"/> <argument name="priceTo" value="$$createProductOne.price$$0"/> </actionGroup> <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> + <waitForElementVisible selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="waitForSearchResultInDefaultView"/> <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> <!-- Assert on storefront custom view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupCustom"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="StorefrontSwitchStoreViewActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="searchByNameCustom"> - <argument name="name" value="$$createProductOne.name$$"/> + <argument name="name" value=""$$createProductOne.name$$""/> <argument name="priceFrom" value="$$createProductOne.price$$0"/> <argument name="priceTo" value="$$createProductOne.price$$0"/> </actionGroup> <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultCustom"/> + <waitForElementVisible selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="waitForSearchResultInCustomView"/> <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInCustom"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index bee13bec370da..18d4b9e341cc6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -18,6 +18,85 @@ <testCaseId value="MC-128"/> <group value="Catalog"/> <group value="Product Attributes"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> + <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> + <createData entity="ApiProductWithDescription" stepKey="createProductTwo"/> + <createData entity="ApiProductNameWithNoSpaces" stepKey="createProductThree"/> + </before> + <after> + <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> + <deleteData createDataKey="createProductThree" stepKey="deleteProductThree"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="AdminDeleteStoreViewActionGroup"/> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + + <!-- Search and select products --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="api-simple-product"/> + </actionGroup> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + <seeInCurrentUrl stepKey="seeInUrl" url="catalog/product_action_attribute/edit/"/> + <!-- Switch store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchStoreViewActionGroup"/> + <!-- Update attribute --> + <click selector="{{AdminEditProductAttributesSection.ChangeAttributeDescriptionToggle}}" stepKey="toggleToChangeDescription"/> + <fillField selector="{{AdminEditProductAttributesSection.AttributeDescription}}" userInput="Updated $$createProductOne.custom_attributes[description]$$" stepKey="fillAttributeDescriptionField"/> + <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + + <!-- Assert on storefront default view with partial word of product name --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault"> + <argument name="name" value="$$createProductOne.name$$"/> + <argument name="description" value="$$createProductOne.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> + <see userInput="2 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> + + <!-- Assert on storefront custom view with partial word of product name --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupCustom"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="StorefrontSwitchStoreViewActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameCustom"> + <argument name="name" value="$$createProductOne.name$$"/> + <argument name="description" value="Updated $$createProductOne.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultCustom"/> + <see userInput="2 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInCustom"/> + + <!-- Assert Storefront default view with exact product name --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault1"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault1"> + <argument name="name" value="$$createProductThree.name$$"/> + <argument name="description" value="$$createProductThree.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault1"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault1"/> + </test> + <test name="AdminMassUpdateProductAttributesStoreViewScopeMysqlTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product attributes"/> + <title value="Admin should be able to mass update product attributes in store view scope using the Mysql search engine"/> + <description value="Admin should be able to mass update product attributes in store view scope using the Mysql search engine"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-20467"/> + <group value="Catalog"/> + <group value="Product Attributes"/> + <group value="SearchEngineMysql"/> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml index e9b54e3f1a3dc..02e8157282dee 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -18,6 +18,157 @@ <testCaseId value="MAGETWO-59361"/> <group value="Catalog"/> <group value="Product Attributes"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + + <!--Create Website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="Second Website"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + + <!--Create Store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="Second Website"/> + <argument name="storeGroupName" value="Second Store"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + + <!--Create Store view --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> + <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> + <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForPageLoad stepKey="waitForPageLoad2" time="180" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" time="150" stepKey="waitForPageReolad"/> + <see userInput="You saved the store view." stepKey="seeSavedMessage" /> + + <!--Create a Simple Product 1 --> + <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct1"> + <argument name="product" value="simpleProductForMassUpdate"/> + <argument name="website" value="Second Website"/> + </actionGroup> + + <!--Create a Simple Product 2 --> + <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct2"> + <argument name="product" value="simpleProductForMassUpdate2"/> + <argument name="website" value="Second Website"/> + </actionGroup> + </before> + <after> + <!--Delete website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + + <!--Delete Products --> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> + <argument name="productName" value="simpleProductForMassUpdate.name"/> + </actionGroup> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct2"> + <argument name="productName" value="simpleProductForMassUpdate2.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> + </after> + + <!-- Search and select products --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="{{simpleProductForMassUpdate.keyword}}"/> + </actionGroup> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + + <!-- Filter to Second Store View --> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterStoreView" > + <argument name="customStore" value="'Second Store View'" /> + </actionGroup> + + <!-- Select Product 2 --> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOption"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabled"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + + <!-- Verify Product Statuses --> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfProduct1IsEnabled"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabled"/> + + <!-- Filter to Default Store View --> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterDefaultStoreView"> + <argument name="customStore" value="'Default'" /> + </actionGroup> + + <!-- Verify Product Statuses --> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct1IsEnabled"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct2IsEnabled"/> + + <!-- Assert on storefront default view with first product --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault"> + <argument name="name" value="{{simpleProductForMassUpdate.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> + + <!-- Assert on storefront default view with second product --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefaultToSearchSecondProduct"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefaultWithSecondProduct"> + <argument name="name" value="{{simpleProductForMassUpdate2.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefaultForSecondProduct"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefaultSecondProductResults"/> + + <!--Enable the product in Default store view--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad2"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckboxDefaultStoreView"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckboxDefaultStoreView2"/> + + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdownDefaultStoreView"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOptionDefaultStoreView"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabledDefaultStoreView"/> + <waitForPageLoad stepKey="waitForBulkUpdatePageDefaultStoreView"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabledDefaultStoreView"/> + + <!-- Assert on storefront default view --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault2"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault2"> + <argument name="name" value="{{simpleProductForMassUpdate.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault2"/> + <see userInput="We can't find any items matching these search criteria." selector="{{StorefrontCatalogSearchAdvancedResultMainSection.message}}" stepKey="seeInDefault2"/> + </test> + <test name="AdminMassUpdateProductStatusStoreViewScopeMysqlTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product status"/> + <title value="Admin should be able to mass update product statuses in store view scope using the Mysql search engine"/> + <description value="Admin should be able to mass update product statuses in store view scope using the Mysql search engine"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-20471"/> + <group value="Catalog"/> + <group value="Product Attributes"/> + <group value="SearchEngineMysql"/> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> @@ -147,5 +298,5 @@ </actionGroup> <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault2"/> <see userInput="We can't find any items matching these search criteria." selector="{{StorefrontCatalogSearchAdvancedResultMainSection.message}}" stepKey="seeInDefault2"/> - </test> -</tests> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index 247711295a555..a8a8ede297b44 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -69,6 +69,10 @@ <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> + <!-- TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Category in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index d17078d794b42..b613068893b0e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -59,6 +59,9 @@ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify category displayed in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index 264615ff6736f..f7fd81f28199f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index ec0d86ac066fd..41d3ca3020c98 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -18,6 +18,9 @@ <testCaseId value="MC-11146"/> <group value="catalog"/> <group value="indexer"/> + <skip> + <issueId value="MC-20392"/> + </skip> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml new file mode 100644 index 0000000000000..bae81513de632 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml @@ -0,0 +1,117 @@ +<?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="AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey"> + <annotations> + <stories value="Product"/> + <features value="Catalog"/> + <title value="Product custom URL Key is preserved when assigned to a Category (without custom URL Key) alongside with another Product without custom URL Key"/> + <description value="The test verifies that product custom URL Key is preserved when assigned to a Category (without custom URL Key) alongside with another Product without custom URL Key."/> + <severity value="MAJOR"/> + <testCaseId value="MC-6443"/> + <useCaseId value="MAGETWO-90331"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create Simple Products --> + <createData entity="SimpleProduct2" stepKey="createSimpleProductFirst"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProductSecond"/> + + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createSimpleProductFirst" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSimpleProductSecond" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open product --> + <amOnPage url="{{AdminProductEditPage.url($createSimpleProductSecond.id$)}}" stepKey="openProductSecondEditPage"/> + <!-- switch store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreView"> + <argument name="storeView" value="storeViewData.name"/> + </actionGroup> + + <!-- set url key --> + <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.urlKeyInput}}" visible="false" stepKey="openSeoSection"/> + <uncheckOption selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="uncheckUseDefaultUrlKey"/> + <fillField userInput="U2" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <actionGroup ref="goToAdminCategoryPageById" stepKey="openCategory"> + <argument name="id" value="$createCategory.id$"/> + </actionGroup> + + <actionGroup ref="AdminCategoryAssignProduct" stepKey="assignSimpleProductFirst"> + <argument name="productSku" value="$createSimpleProductFirst.sku$"/> + </actionGroup> + <actionGroup ref="AdminCategoryAssignProduct" stepKey="assignSimpleProductSecond"> + <argument name="productSku" value="$createSimpleProductSecond.sku$"/> + </actionGroup> + + <actionGroup ref="saveCategoryForm" stepKey="saveCategory"/> + + <executeJS function="return '$createCategory.name$'.toLowerCase();" stepKey="categoryNameLower" /> + <executeJS function="return '$createSimpleProductFirst.name$'.toLowerCase();" stepKey="simpleProductFirstNameLower" /> + <executeJS function="return '$createSimpleProductSecond.name$'.toLowerCase();" stepKey="simpleProductSecondNameLower" /> + + <!-- Make assertions on frontend --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($createCategory.name$)}}" stepKey="onCategoryPage"/> + <seeInCurrentUrl url="{$categoryNameLower}.html" stepKey="checkCategryUrlKey"/> + + <!-- Open first product --> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($createSimpleProductFirst.name$)}}" stepKey="openFirstProduct"/> + <waitForPageLoad time="30" stepKey="waitForFirstProduct"/> + <seeInCurrentUrl url="{$simpleProductFirstNameLower}.html" stepKey="checkFirstSimpleProductUrlKey"/> + + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="onCategoryView"/> + <!-- Open second product --> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($createSimpleProductSecond.name$)}}" stepKey="openSecondProduct"/> + <waitForPageLoad time="30" stepKey="waitForSecondProduct"/> + <seeInCurrentUrl url="{$simpleProductSecondNameLower}.html" stepKey="checkSecondSimpleProductUrlKey"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> + <argument name="storeView" value="storeViewData"/> + </actionGroup> + + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($createCategory.name$)}}" stepKey="openCategoryPage"/> + <seeInCurrentUrl url="{$categoryNameLower}.html" stepKey="seeCategoryUrlKey"/> + + <!-- Open product first --> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProductFirst.name$$)}}" stepKey="openFirstSimpleProduct"/> + <waitForPageLoad time="30" stepKey="waitForFirstSimpleProduct"/> + <seeInCurrentUrl url="{$simpleProductFirstNameLower}.html" stepKey="assertFirstSimpleProductUrlKey"/> + + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($createCategory.name$)}}" stepKey="openCategoryView"/> + <!-- Open product2 --> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($createSimpleProductSecond.name$)}}" stepKey="openSecondSimpleProduct"/> + <waitForPageLoad time="30" stepKey="waitForSecondSimpleProduct"/> + <seeInCurrentUrl url="u2.html" stepKey="assertSecondSimpleProductUrlKey"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml new file mode 100644 index 0000000000000..72c270aad585c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml @@ -0,0 +1,105 @@ +<?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="AdminProductGridFilteringByCustomAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product grid"/> + <title value="Sorting the product grid by custom product attribute"/> + <description value="Sorting the product grid by custom product attribute should sort Alphabetically instead of value id"/> + <severity value="MAJOR"/> + <useCaseId value="MC-19031"/> + <testCaseId value="MC-20329"/> + <group value="catalog"/> + </annotations> + <before> + <!--Login as admin and delete all products --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> + <!--Create dropdown product attribute--> + <createData entity="productDropDownAttribute" stepKey="createDropdownAttribute"/> + <!--Create attribute options--> + <createData entity="ProductAttributeOption7" stepKey="createFirstProductAttributeOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <createData entity="ProductAttributeOption8" stepKey="createSecondProductAttributeOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <createData entity="ProductAttributeOption9" stepKey="createThirdProductAttributeOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <!--Add attribute to default attribute set--> + <createData entity="AddToDefaultSet" stepKey="addAttributeToDefaultSet"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create 3 products--> + <createData entity="ApiSimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Update first product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForFirstProduct"> + <argument name="product" value="$$createFirstProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="editFirstProduct"> + <argument name="product" value="$$createFirstProduct$$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($$createDropdownAttribute.attribute[attribute_code]$$)}}" userInput="$$createFirstProductAttributeOption.option[store_labels][0][label]$$" stepKey="setFirstAttributeValue"/> + <actionGroup ref="saveProductForm" stepKey="saveFirstProduct"/> + <!--Update second product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSecondProduct"> + <argument name="product" value="$$createSecondProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="editSecondProduct"> + <argument name="product" value="$$createSecondProduct$$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($$createDropdownAttribute.attribute[attribute_code]$$)}}" userInput="$$createSecondProductAttributeOption.option[store_labels][0][label]$$" stepKey="setSecondAttributeValue"/> + <actionGroup ref="saveProductForm" stepKey="saveSecondProduct"/> + <!--Update third product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForThirdProduct"> + <argument name="product" value="$$createThirdProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="editThirdProduct"> + <argument name="product" value="$$createThirdProduct$$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField($$createDropdownAttribute.attribute[attribute_code]$$)}}" userInput="$$createThirdProductAttributeOption.option[store_labels][0][label]$$" stepKey="setThirdAttributeValue"/> + <actionGroup ref="saveProductForm" stepKey="saveThirdProduct"/> + </before> + <after> + <!--Delete products--> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <!--Delete attribute--> + <deleteData createDataKey="createDropdownAttribute" stepKey="deleteDropdownAttribute"/> + <!--Delete category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="NavigateToAndResetProductGridToDefaultView" stepKey="NavigateToAndResetProductGridToDefaultViewAfterTest"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductGridFilters"/> + <!--Sort by custom attribute DESC using grabbed value--> + <conditionalClick selector="{{AdminProductGridSection.columnHeader($$createDropdownAttribute.attribute[frontend_labels][0][label]$$)}}" dependentSelector="{{AdminProductGridSection.columnHeader($$createDropdownAttribute.attribute[frontend_labels][0][label]$$)}}" visible="true" stepKey="ascendSortByCustomAttribute"/> + <waitForPageLoad stepKey="waitForProductGridLoad"/> + <!--Check products sorting. Expected result => Blue-Green-Red --> + <see selector="{{AdminProductGridSection.productGridNameProduct($$createSecondProduct.name$$)}}" userInput="$$createSecondProduct.name$$" stepKey="seeSecondProductName"/> + <see selector="{{AdminProductGridSection.productGridNameProduct($$createFirstProduct.name$$)}}" userInput="$$createFirstProduct.name$$" stepKey="seeFirstProductName"/> + <see selector="{{AdminProductGridSection.productGridNameProduct($$createThirdProduct.name$$)}}" userInput="$$createThirdProduct.name$$" stepKey="seeThirdProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml new file mode 100644 index 0000000000000..de065d2d930cb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml @@ -0,0 +1,100 @@ +<?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="AdminVirtualProductTypeSwitchingToDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product type switching"/> + <title value="Virtual product type switching on editing to Downloadable product"/> + <description value="Virtual product type switching on editing to Downloadable product"/> + <testCaseId value="MC-17954"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="VirtualProduct" stepKey="createProduct"/> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Change product type to Downloadable--> + <comment userInput="Change product type to Downloadable" stepKey="commentCreateDownloadable"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForDownloadableProductPageLoad"/> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkOptionPurchaseSeparately"/> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveDownloadableProductForm"/> + <!--Assert downloadable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertDownloadableProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> + <!--Assert downloadable product on storefront--> + <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontDownloadableProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertDownloadableProductInStock"/> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinksInStorefront"/> + <seeElement selector="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeDownloadableLink" /> + </test> + <test name="AdminDownloadableProductTypeSwitchingToSimpleProductTest" extends="AdminVirtualProductTypeSwitchingToDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product type switching"/> + <title value="Downloadable product type switching on editing to Simple product"/> + <description value="Downloadable product type switching on editing to Simple product"/> + <testCaseId value="MC-17955"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <!--Change product type to Simple--> + <comment userInput="Change product type to Simple Product" stepKey="commentCreateSimple"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeightForProduct"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + <!--Assert simple product on Admin product page grid--> + <comment userInput="Assert simple product in Admin product page grid" stepKey="commentAssertProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogSimpleProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterSimpleProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeSimpleProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeSimpleProductTypeInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearSimpleProductFilters"/> + <!--Assert simple product on storefront--> + <comment userInput="Assert simple product on storefront" stepKey="commentAssertSimpleProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openSimpleProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontSimpleProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertSimpleProductInStock"/> + <dontSeeElement selector="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLinkWithMaxDownloads.title)}}" stepKey="dontSeeDownloadableLink" /> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml index 240a5492355cf..ebae27a1f7182 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -18,7 +18,7 @@ <group value="Catalog"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml index 1cd0e15780c11..df50edd20410a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml @@ -34,7 +34,7 @@ <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Go to the first product edit page --> @@ -144,6 +144,8 @@ <!-- Save the second product --> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!-- Go to the admin grid and see the uploaded image --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> @@ -186,7 +188,7 @@ <after> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Go to the product edit page --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml index 2ff83afa15e5e..0f63a72844452 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> <argument name="categoryEntity" value="_defaultCategory"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create category, change store view to default --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml index 1cb01ac11cb8f..ad110ceee32d2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -64,6 +64,10 @@ <!--Verify Category Title--> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Verify Category in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> <waitForPageLoad stepKey="waitForPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml index f317c66e5366a..10ff1151cd9b3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -25,6 +25,10 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + + <!--TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index 637ae790c16c8..82395e5d6e0eb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -94,6 +94,9 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 045b3f3420ff6..4817b3497c97e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -94,6 +94,9 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml index 8ac56d09e5b42..9fa0e155a4fe7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualProductRegularPrice"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml index a2a4f65860254..bbde98f8fc043 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteDefaultVirtualProduct"> + <argument name="sku" value="{{updateVirtualProductRegularPrice5OutOfStock.sku}}"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml index ffbad0752b73c..15e5be210b73a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteDefaultVirtualProduct"> + <argument name="sku" value="{{updateVirtualProductRegularPrice5OutOfStock.sku}}"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml index aa3184994daff..8b4518007ea29 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml @@ -27,6 +27,9 @@ </createData> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteDefaultVirtualProduct"> + <argument name="sku" value="{{updateVirtualProductRegularPrice99OutOfStock.sku}}"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml index 3101c1e460322..984c296845113 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteDefaultVirtualProduct"> + <argument name="sku" value="{{updateVirtualProductSpecialPrice.sku}}"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 58978c31b5b40..1c590563d4cfc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteDefaultVirtualProduct"> + <argument name="sku" value="{{updateVirtualProductSpecialPriceOutOfStock.sku}}"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml index d28e9ddbb1271..e0e8360850983 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualProductTierPriceInStock"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml index 22dd2b0054db4..677cc4c65ce88 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualProductWithTierPriceInStock"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 29c7536d21621..f0148f3d384c1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualTierPriceOutOfStock"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml index a4c8b492d9d84..d8fa20c7cc469 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml @@ -19,10 +19,13 @@ <group value="Catalog"/> </annotations> <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> <createData entity="ApiProductWithDescription" stepKey="product"/> </before> <after> <deleteData createDataKey="product" stepKey="delete"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> </after> </test> <test name="AdvanceCatalogSearchSimpleProductBySkuTest"> @@ -36,7 +39,7 @@ <group value="Catalog"/> </annotations> <before> - <createData entity="ApiProductWithDescription" stepKey="product"/> + <createData entity="ApiProductWithDescriptionAndUnderscoredSku" stepKey="product"/> </before> <after> <deleteData createDataKey="product" stepKey="delete"/> @@ -87,6 +90,9 @@ <group value="Catalog"/> </annotations> <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> + <createData entity="ApiProductWithDescription" stepKey="product"/> <getData entity="GetProduct" stepKey="arg1"> <requiredEntity createDataKey="product"/> @@ -100,6 +106,7 @@ </before> <after> <deleteData createDataKey="product" stepKey="delete"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> </after> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml index 84c3f81ef6dbf..07b802637b2e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml @@ -33,7 +33,7 @@ <group value="Catalog"/> </annotations> <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> + <createData entity="ApiVirtualProductWithDescriptionAndUnderscoredSku" stepKey="product"/> </before> </test> <test name="AdvanceCatalogSearchVirtualProductByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index 5cae81b36a323..674d46b9c18b1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -36,7 +36,7 @@ <createData entity="NewRootCategory" stepKey="createNewRootCategoryA"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml index 5a94dd4f04d24..02df8db5c2121 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml @@ -11,6 +11,7 @@ <test name="DisplayRefreshCacheAfterChangingCategoryPageLayoutTest"> <annotations> <features value="Catalog"/> + <stories value="Category Layout Change"/> <title value="'Refresh cache' admin notification is displayed when changing category page layout"/> <description value="'Refresh cache' message is not displayed when changing category page layout"/> <severity value="MAJOR"/> @@ -45,7 +46,8 @@ <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> <waitForPageLoad stepKey="waitSaveToApply"/> <!-- See if warning message displays --> - <comment userInput="See if warning message displays" stepKey="checkWarningMessagePresence"/> - <see selector="{{AdminMessagesSection.warningMessage}}" userInput="Please go to Cache Management and refresh cache types" stepKey="seeWarningMessage"/> + <actionGroup ref="AdminSystemMessagesWarningActionGroup" stepKey="seeWarningMessage"> + <argument name="message" value="Please go to Cache Management and refresh cache types"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 7c0de6da18caf..ad66214e902fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -17,6 +17,203 @@ <description value="User browses catalog, searches for product, adds product to cart, adds product to wishlist, compares products, uses coupon code and checks out."/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-87435"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> + + <createData entity="ApiCategory" stepKey="createCategory"/> + + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createSimpleProduct1Image"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createSimpleProduct1Image1"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct1" createDataKey="createSimpleProduct1"/> + + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createSimpleProduct2Image"> + <requiredEntity createDataKey="createSimpleProduct2"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct2" createDataKey="createSimpleProduct2"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createSimpleProduct1Image" stepKey="deleteSimpleProduct1Image"/>--> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createSimpleProduct1Image1" stepKey="deleteSimpleProduct1Image1"/>--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createSimpleProduct2Image" stepKey="deleteSimpleProduct2Image"/>--> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + + <!--Re-index--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!-- Step 1: User browses catalog --> + <comment userInput="Start of browsing catalog" stepKey="startOfBrowsingCatalog" /> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <waitForPageLoad stepKey="homeWaitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeWaitForWelcomeMessage"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome"/> + + <!-- Open Category --> + <comment userInput="Open category" stepKey="commentOpenCategory" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="browseClickCategory"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="browseAssertCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <!-- Check simple product 1 in category --> + <comment userInput="Check simple product 1 in category" stepKey="commentCheckSimpleProductInCategory" /> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="browseGrabSimpleProduct1ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$browseGrabSimpleProduct1ImageSrc" stepKey="browseAssertSimpleProduct1ImageNotDefault"/> + <!-- Check simple product 2 in category --> + <comment userInput="Check simple product 2 in category" stepKey="commentCheckSimpleProduct2InCategory" /> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="browseGrabSimpleProduct2ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$browseGrabSimpleProduct2ImageSrc" stepKey="browseAssertSimpleProduct2ImageNotDefault"/> + + <!-- View Simple Product 1 --> + <comment userInput="View simple product 1" stepKey="commentViewSimpleProduct1" after="browseAssertSimpleProduct2ImageNotDefault"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="browseClickCategorySimpleProduct1View" after="commentViewSimpleProduct1"/> + <waitForLoadingMaskToDisappear stepKey="waitForSimpleProduct1Viewloaded" /> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct1Page"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="browseGrabSimpleProduct1PageImageSrc"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$browseGrabSimpleProduct1PageImageSrc" stepKey="browseAssertSimpleProduct1PageImageNotDefault"/> + + <!-- View Simple Product 2 --> + <comment userInput="View simple product 2" stepKey="commentViewSimpleProduct2" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickCategory1"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct2.name$$)}}" stepKey="browseClickCategorySimpleProduct2View"/> + <waitForLoadingMaskToDisappear stepKey="waitForSimpleProduct2ViewLoaded" /> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct2Page"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="browseGrabSimpleProduct2PageImageSrc"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$browseGrabSimpleProduct2PageImageSrc" stepKey="browseAssertSimpleProduct2PageImageNotDefault"/> + <comment userInput="End of browsing catalog" stepKey="endOfBrowsingCatalog" after="browseAssertSimpleProduct2PageImageNotDefault"/> + + <!-- Step 4: User compares products --> + <comment userInput="Start of comparing products" stepKey="startOfComparingProducts" after="endOfBrowsingCatalog"/> + <!-- Add Simple Product 1 to comparison --> + <comment userInput="Add simple product 1 to comparison" stepKey="commentAddSimpleProduct1ToComparison" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="compareClickCategory" /> + <waitForLoadingMaskToDisappear stepKey="waitForCategoryloaded" /> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="compareAssertCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="compareAssertSimpleProduct1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct1ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct1ImageSrc" stepKey="compareAssertSimpleProduct1ImageNotDefault"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="compareClickSimpleProduct1"/> + <waitForLoadingMaskToDisappear stepKey="waitForCompareSimpleProduct1loaded" /> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="compareAssertProduct1Page"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="compareGrabSimpleProduct1PageImageSrc"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$compareGrabSimpleProduct1PageImageSrc" stepKey="compareAssertSimpleProduct2PageImageNotDefault"/> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="compareAddSimpleProduct1ToCompare"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + + <!-- Add Simple Product 2 to comparison --> + <comment userInput="Add simple product 2 to comparison" stepKey="commentAddSimpleProduct2ToComparison" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="compareClickCategory1"/> + <waitForLoadingMaskToDisappear stepKey="waitForCompareCategory1loaded" /> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="compareAssertCategory1"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="compareAssertSimpleProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct2ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct2ImageSrc" stepKey="compareAssertSimpleProduct2ImageNotDefault"/> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="compareAddSimpleProduct2ToCompare"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + + <!-- Check products in comparison sidebar --> + <!-- Check simple product 1 in comparison sidebar --> + <comment userInput="Check simple product 1 in comparison sidebar" stepKey="commentCheckSimpleProduct1InComparisonSidebar" after="compareAddSimpleProduct2ToCompare"/> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareSimpleProduct1InSidebar" after="commentCheckSimpleProduct1InComparisonSidebar"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- Check simple product 2 in comparison sidebar --> + <comment userInput="Check simple product 2 in comparison sidebar" stepKey="commentCheckSimpleProduct2InComparisonSidebar" /> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareSimpleProduct2InSidebar"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + + <!-- Check products on comparison page --> + <!-- Check simple product 1 on comparison page --> + <comment userInput="Check simple product 1 on comparison page" stepKey="commentCheckSimpleProduct1OnComparisonPage" after="compareSimpleProduct2InSidebar"/> + <actionGroup ref="StorefrontOpenAndCheckComparisionActionGroup" stepKey="compareOpenComparePage" after="commentCheckSimpleProduct1OnComparisonPage"/> + <actionGroup ref="StorefrontCheckCompareSimpleProductActionGroup" stepKey="compareAssertSimpleProduct1InComparison"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct1ImageSrcInComparison"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct1ImageSrcInComparison" stepKey="compareAssertSimpleProduct1ImageNotDefaultInComparison"/> + <!-- Check simple product2 on comparison page --> + <comment userInput="Check simple product 2 on comparison page" stepKey="commentCheckSimpleProduct2OnComparisonPage" /> + <actionGroup ref="StorefrontCheckCompareSimpleProductActionGroup" stepKey="compareAssertSimpleProduct2InComparison"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct2ImageSrcInComparison"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct2ImageSrcInComparison" stepKey="compareAssertSimpleProduct2ImageNotDefaultInComparison"/> + + <!-- Clear comparison sidebar --> + <comment userInput="Clear comparison sidebar" stepKey="commentClearComparisonSidebar" after="compareAssertSimpleProduct2ImageNotDefaultInComparison"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="compareClickCategoryBeforeClear" after="commentClearComparisonSidebar"/> + + + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="compareAssertCategory2"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="compareClearCompare"/> + <comment userInput="End of Comparing Products" stepKey="endOfComparingProducts" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <annotations> + <features value="End to End scenarios"/> + <stories value="B2C guest user - MAGETWO-75411"/> + <group value="e2e"/> + <title value="You should be able to pass End to End B2C Guest User scenario using the Mysql search engine"/> + <description value="User browses catalog, searches for product, adds product to cart, adds product to wishlist, compares products, uses coupon code and checks out using the Mysql search engine."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-20476"/> + <group value="SearchEngineMysql"/> </annotations> <before> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml index 461ebde29fcad..c8a7cdee66b53 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -49,6 +49,10 @@ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> <waitForPageLoad stepKey="waitForCategorySaved"/> <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml index e9e9eb0158789..8092b03c53cba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -53,7 +53,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..3b1cd7ff02e6a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml @@ -0,0 +1,29 @@ +<?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="StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search simple product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search simple product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20361"/> + <group value="Catalog"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..d6b3a060ffd3a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml @@ -0,0 +1,26 @@ +<?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="StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search virtual product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search virtual product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20385"/> + <group value="Catalog"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> + </before> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml new file mode 100644 index 0000000000000..ae54b72a5a702 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -0,0 +1,336 @@ +<?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="StorefrontCatalogNavigationMenuUIDesktopTest"> + <annotations> + <features value="Catalog"/> + <stories value="Storefront Catalog Navigation Menu UI"/> + <title value="Storefront Catalog Navigation Menu UI, desktop"/> + <description value="Verify UI of Navigation Menu functionality on Storefront"/> + <testCaseId value="MC-11329"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="theme"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="DeleteDefaultCategoryChildren" stepKey="deleteRootCategoryChildren"/> + </before> + <after> + <actionGroup ref="DeleteDefaultCategoryChildren" stepKey="deleteRootCategoryChildren"/> + <actionGroup ref="AdminChangeStorefrontThemeActionGroup" stepKey="changeThemeToDefault"> + <argument name="theme" value="{{MagentoLumaTheme.name}}"/> + </actionGroup> + <!-- Admin log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to Content > Themes. Change theme to Blank --> + <actionGroup ref="AdminChangeStorefrontThemeActionGroup" stepKey="changeThemeToBlank"> + <argument name="theme" value="{{MagentoBlankTheme.name}}"/> + </actionGroup> + + <!-- Open storefront --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> + + <!-- Assert no category - no menu --> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.navigationMenu}}" stepKey="dontSeeMenu"/> + + <!-- Assert single row - no hover state --> + <createData entity="ApiCategory" stepKey="createFirstCategoryBlank"> + <field key="name">Category A</field> + </createData> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForBlankSingleRowAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryBlank.name$$)}}" stepKey="hoverFirstCategoryBlank"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}" stepKey="assertNoHoverState"/> + + <!-- Create categories --> + <createData entity="ApiCategory" stepKey="createSecondCategoryBlank"> + <field key="name">TEST</field> + </createData> + <createData entity="ApiCategory" stepKey="createThirdCategoryBlank"> + <field key="name">_test2</field> + </createData> + <createData entity="ApiCategory" stepKey="createFourthCategoryBlank"> + <field key="name">test 3</field> + </createData> + <createData entity="ApiCategory" stepKey="createFifthCategoryBlank"> + <field key="name">Category with several products</field> + </createData> + <createData entity="ApiCategory" stepKey="createSixthCategoryBlank"> + <field key="name">test 5</field> + </createData> + <createData entity="ApiCategory" stepKey="createSeventhCategoryBlank"> + <field key="name">test 8</field> + </createData> + <createData entity="ApiCategory" stepKey="createEighthCategoryBlank"> + <field key="name">This is a very very very very very looong title</field> + </createData> + <createData entity="ApiCategory" stepKey="createNinthCategoryBlank"> + <field key="name">test 6</field> + </createData> + <createData entity="ApiCategory" stepKey="createTenthCategoryBlank"> + <field key="name">test 7</field> + </createData> + <createData entity="ApiCategory" stepKey="createEleventhCategoryBlank"> + <field key="name">test 4</field> + </createData> + <createData entity="ApiCategory" stepKey="createTwelfthCategoryBlank"> + <field key="name">Category with image</field> + </createData> + <createData entity="ApiCategory" stepKey="createThirteenthCategoryBlank"> + <field key="name">test 0</field> + </createData> + <createData entity="ApiCategory" stepKey="createCategoryWithoutChildrenBlank"> + <field key="name">Category with description & custom title</field> + </createData> + <createData entity="ApiCategory" stepKey="createCategoryWithChildrenBlank"> + <field key="name">Category with children</field> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelOneBlank"> + <field key="name">level 1 test category very very very long name</field> + <requiredEntity createDataKey="createCategoryWithChildrenBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelOneBlank"> + <field key="name">level 1 test category name</field> + <requiredEntity createDataKey="createCategoryWithChildrenBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createThirdCategoryLevelOneBlank"> + <field key="name">level 1 with children</field> + <requiredEntity createDataKey="createCategoryWithChildrenBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelTwoBlank"> + <field key="name">level 2 with children</field> + <requiredEntity createDataKey="createThirdCategoryLevelOneBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelThreeBlank"> + <field key="name">level 3 test</field> + <requiredEntity createDataKey="createCategoryLevelTwoBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelFourBlank"> + <field key="name">level 4</field> + <requiredEntity createDataKey="createCategoryLevelThreeBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelFourBlank"> + <field key="name">level 4 test</field> + <requiredEntity createDataKey="createCategoryLevelThreeBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelFiveBlank"> + <field key="name">level 5</field> + <requiredEntity createDataKey="createSecondCategoryLevelFourBlank"/> + </createData> + + <!-- Several rows. Hover on category without children --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForBlankSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithoutChildrenBlank.name$$)}}" stepKey="hoverCategoryWithoutChildren"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createCategoryWithoutChildrenBlank.name$$, 'level0')}}" stepKey="dontSeeChildrenInCategory"/> + + <!-- Nested level 1. No hover state --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenBlank.name$$)}}" stepKey="hoverCategoryWithChildrenTopLevel"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkNoHoverState"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemByLevel('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.white}}"/> + </actionGroup> + + <!-- Nested level 1. Hover state on 1st item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryLevelOneBlank.name$$)}}" stepKey="hoverCategoryLevelOneFirstItem"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverFirstItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Nested level 1 & 2. Hover state on the last item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createThirdCategoryLevelOneBlank.name$$)}}" stepKey="hoverCategoryLevelOneLastItem"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverLastItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Submenu appears rightward --> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level0')}}" stepKey="assertTopLevelMenuLeftDirection"/> + + <!-- Nested level 1 & 5 --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelTwoBlank.name$$)}}" stepKey="hoverCategoryLevelTwo"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuLeftDirection('level1')}}" stepKey="seeLevelOneMenuLeftDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelThreeBlank.name$$)}}" stepKey="hoverCategoryLevelThree"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuLeftDirection('level2')}}" stepKey="seeLevelTwoMenuRightDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategoryLevelFourBlank.name$$)}}" stepKey="hoverCategoryLevelFour"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level3')}}" stepKey="seeLevelThreeMenuRightDirection"/> + + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkSubcategoryHighlighted"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level3')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Delete all creation for Blank theme --> + <deleteData createDataKey="createFirstCategoryBlank" stepKey="deleteFirstCategoryBlank"/> + <deleteData createDataKey="createSecondCategoryBlank" stepKey="deleteSecondCategoryBlank"/> + <deleteData createDataKey="createThirdCategoryBlank" stepKey="deleteThirdCategoryBlank"/> + <deleteData createDataKey="createFourthCategoryBlank" stepKey="deleteFourthCategoryBlank"/> + <deleteData createDataKey="createFifthCategoryBlank" stepKey="deleteFifthCategoryBlank"/> + <deleteData createDataKey="createSixthCategoryBlank" stepKey="deleteSixthCategoryBlank"/> + <deleteData createDataKey="createSeventhCategoryBlank" stepKey="deleteSeventhCategoryBlank"/> + <deleteData createDataKey="createEighthCategoryBlank" stepKey="deleteEighthCategoryBlank"/> + <deleteData createDataKey="createNinthCategoryBlank" stepKey="deleteNinthCategoryBlank"/> + <deleteData createDataKey="createTenthCategoryBlank" stepKey="deleteTenthCategoryBlank"/> + <deleteData createDataKey="createEleventhCategoryBlank" stepKey="deleteEleventhCategoryBlank"/> + <deleteData createDataKey="createTwelfthCategoryBlank" stepKey="deleteTwelfthCategoryBlank"/> + <deleteData createDataKey="createThirteenthCategoryBlank" stepKey="deleteThirteenthCategoryBlank"/> + <deleteData createDataKey="createCategoryWithChildrenBlank" stepKey="deleteCategoryWithChildrenBlank"/> + <deleteData createDataKey="createCategoryWithoutChildrenBlank" stepKey="deleteCategoryWithoutChildrenBlank"/> + + <!-- Go to Content > Themes. Change theme to Luma --> + <actionGroup ref="AdminChangeStorefrontThemeActionGroup" stepKey="changeThemeToLuma"> + <argument name="theme" value="{{MagentoLumaTheme.name}}"/> + </actionGroup> + + <!-- Open storefront --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefront"/> + + <!-- Assert no category - no menu --> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.navigationMenu}}" stepKey="dontSeeMenuOnStorefront"/> + + <!-- Create categories --> + <createData entity="ApiCategory" stepKey="createFirstCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createSecondCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createThirdCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createFourthCategoryLuma"/> + + <!-- Single row. No hover state --> + <reloadPage stepKey="reload"/> + <waitForPageLoad stepKey="waitForLumaSingleRowAppear"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFirstCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFirstCategory"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createSecondCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInSecondCategory"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createThirdCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateThirdCategory"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFourthCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFourthCategory"/> + + <!-- Create categories for testing Luma theme --> + <createData entity="ApiCategory" stepKey="createFifthCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createCategoryWithChildrenLuma"/> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelOneLuma"> + <requiredEntity createDataKey="createCategoryWithChildrenLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelOneLuma"> + <requiredEntity createDataKey="createCategoryWithChildrenLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createThirdCategoryLevelOneLuma"> + <requiredEntity createDataKey="createCategoryWithChildrenLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelTwoLuma"> + <requiredEntity createDataKey="createThirdCategoryLevelOneLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelTwoLuma"> + <requiredEntity createDataKey="createThirdCategoryLevelOneLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelThreeLuma"> + <requiredEntity createDataKey="createSecondCategoryLevelTwoLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelFourLuma"> + <requiredEntity createDataKey="createCategoryLevelThreeLuma"/> + </createData> + <createData entity="ApiCategory" stepKey="createSixthCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createSeventhCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createEighthCategoryLuma"/> + + <!-- Several rows. Hover on Category without children --> + <reloadPage stepKey="refresh"/> + <waitForPageLoad stepKey="waitForLumaSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFifthCategoryLuma.name$$)}}" stepKey="hoverOnCategoryWithoutChildren"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFifthCategoryLuma.name$$, 'level0')}}" stepKey="dontSeeSubcategoriesInCategory"/> + + <!-- Nested level 1. No hover state --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenLuma.name$$)}}" stepKey="hoverOnCategoryWithChildren"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkNoHighlightedInSubmenuAfterHover"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemByLevel('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.white}}"/> + </actionGroup> + + <!-- Nested level 1. Hover state on first item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryLevelOneLuma.name$$)}}" stepKey="hoverOnFirstItemLevelOne"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverOnFirstItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Nested levels 1 & 2. Hover state on last item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createThirdCategoryLevelOneLuma.name$$)}}" stepKey="hoverOnLastItemLevelOne"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverOnLastItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Submenu appears rightward --> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level0')}}" stepKey="seeTopLevelRightDirection"/> + + <!-- Nested levels 1 & 5 --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategoryLevelTwoLuma.name$$)}}" stepKey="hoverThirdCategoryLevelTwo"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level1')}}" stepKey="seeFirstLevelRightDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelThreeLuma.name$$)}}" stepKey="hoverOnCategoryLevelThree"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level2')}}" stepKey="seeSecondLevelRightDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelFourLuma.name$$)}}" stepKey="hoverOnCategoryLevelFour"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level3')}}" stepKey="seeThirdLevelRightDirection"/> + + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkSubcategoryHighlightedAfterHover"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level3')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Selected 1st level category --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenLuma.name$$)}}" stepKey="openTopLevelCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageLoaded"/> + + <!-- Assert category active state --> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkCategoryActiveState"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.itemActiveState}}"/> + <argument name="property" value="border-color"/> + <argument name="color" value="{{NavigationMenuColor.orange}}"/> + </actionGroup> + + <!-- Selected subcategory. Assert active state --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openSubcategory"> + <argument name="categoryName" value="$$createCategoryWithChildrenLuma.name$$"/> + <argument name="subCategoryName" value="$$createThirdCategoryLevelOneLuma.name$$"/> + </actionGroup> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenLuma.name$$)}}" stepKey="hoverOnCategory"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createThirdCategoryLevelOneLuma.name$$)}}" stepKey="hoverOnSubcategory"/> + + <!-- Assert subcategory active state --> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkSubitemActiveState"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemActiveState}}"/> + <argument name="property" value="border-color"/> + <argument name="color" value="{{NavigationMenuColor.orange}}"/> + </actionGroup> + + <!-- Delete created category --> + <deleteData createDataKey="createFirstCategoryLuma" stepKey="deleteFirstCategoryLuma"/> + <deleteData createDataKey="createSecondCategoryLuma" stepKey="deleteSecondCategoryLuma"/> + <deleteData createDataKey="createThirdCategoryLuma" stepKey="deleteThirdCategoryLuma"/> + <deleteData createDataKey="createFourthCategoryLuma" stepKey="deleteFourthCategoryLuma"/> + <deleteData createDataKey="createFifthCategoryLuma" stepKey="deleteFifthCategoryLuma"/> + <deleteData createDataKey="createSixthCategoryLuma" stepKey="deleteSixthCategoryLuma"/> + <deleteData createDataKey="createSeventhCategoryLuma" stepKey="deleteSeventhCategoryLuma"/> + <deleteData createDataKey="createEighthCategoryLuma" stepKey="deleteEighthCategoryLuma"/> + <deleteData createDataKey="createCategoryWithChildrenLuma" stepKey="deleteCategoryWithChildrenLuma"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml new file mode 100644 index 0000000000000..3e72df9133898 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml @@ -0,0 +1,85 @@ +<?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="StorefrontCategoryHighlightedAndProductDisplayedTest"> + <annotations> + <features value="Catalog"/> + <stories value="Category"/> + <title value="Сheck that current category is highlighted and all products displayed for it"/> + <description value="Сheck that current category is highlighted and all products displayed for it"/> + <severity value="MAJOR"/> + <testCaseId value="MC-19626"/> + <useCaseId value="MAGETWO-98748"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="SimpleSubCategory" stepKey="category2"/> + <createData entity="SimpleSubCategory" stepKey="category3"/> + <createData entity="SimpleSubCategory" stepKey="category4"/> + <createData entity="SimpleProduct" stepKey="product1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="product2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="product3"> + <requiredEntity createDataKey="category2"/> + </createData> + <createData entity="SimpleProduct" stepKey="product4"> + <requiredEntity createDataKey="category2"/> + </createData> + </before> + <after> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + <deleteData createDataKey="product3" stepKey="deleteProduct3"/> + <deleteData createDataKey="product4" stepKey="deleteProduct4"/> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <deleteData createDataKey="category2" stepKey="deleteCategory2"/> + <deleteData createDataKey="category3" stepKey="deleteCategory3"/> + <deleteData createDataKey="category4" stepKey="deleteCategory4"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Storefront home page--> + <comment userInput="Open Storefront home page" stepKey="openStorefrontHomePage"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontHomePage"/> + <waitForPageLoad stepKey="waitForSimpleProductPage"/> + <!--Click on first category--> + <comment userInput="Click on first category" stepKey="openFirstCategoryPage"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$category1.name$$)}}" stepKey="clickCategory1Name"/> + <waitForPageLoad stepKey="waitForCategory1Page"/> + <!--Check if current category is highlighted and the others are not--> + <comment userInput="Check if current category is highlighted and the others are not" stepKey="checkCateg1NameIsHighlighted"/> + <grabAttributeFrom selector="{{AdminCategorySidebarTreeSection.categoryHighlighted($$category1.name$$)}}" userInput="class" stepKey="grabCategory1Class"/> + <assertContains expectedType="string" expected="active" actual="$grabCategory1Class" stepKey="assertCategory1IsHighlighted"/> + <executeJS function="return document.querySelectorAll('{{AdminCategorySidebarTreeSection.categoryNotHighlighted}}').length" stepKey="highlightedAmount"/> + <assertEquals expectedType="int" expected="1" actual="$highlightedAmount" stepKey="assertRestCategories1IsNotHighlighted"/> + <!--See products in the category page--> + <comment userInput="See products in the category page" stepKey="seeProductsInCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product1.name$)}}" stepKey="seeProduct1InCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product2.name$)}}" stepKey="seeProduct2InCategoryPage"/> + <!--Click on second category--> + <comment userInput="Click on second category" stepKey="openSecondCategoryPage"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$category2.name$$)}}" stepKey="clickCategory2Name"/> + <waitForPageLoad stepKey="waitForCategory2Page"/> + <!--Check if current category is highlighted and the others are not--> + <comment userInput="Check if current category is highlighted and the others are not" stepKey="checkCateg2NameIsHighlighted"/> + <grabAttributeFrom selector="{{AdminCategorySidebarTreeSection.categoryHighlighted($$category2.name$$)}}" userInput="class" stepKey="grabCategory2Class"/> + <assertContains expectedType="string" expected="active" actual="$grabCategory2Class" stepKey="assertCategory2IsHighlighted"/> + <executeJS function="return document.querySelectorAll('{{AdminCategorySidebarTreeSection.categoryNotHighlighted}}').length" stepKey="highlightedAmount2"/> + <assertEquals expectedType="int" expected="1" actual="$highlightedAmount2" stepKey="assertRestCategories1IsNotHighlighted2"/> + <!--Assert products in second category page--> + <comment userInput="Assert products in second category page" stepKey="commentAssertProducts"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product3.name$)}}" stepKey="seeProduct3InCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product4.name$)}}" stepKey="seeProduct4InCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml new file mode 100644 index 0000000000000..4eef6a2c06800 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -0,0 +1,206 @@ +<?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="StorefrontCheckDefaultNumbersProductsToDisplayTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product grid"/> + <title value="Check default numbers: products to display"/> + <description value="Check default numbers: products to display"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17386"/> + <useCaseId value="MC-15341"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Login as Admin --> + <comment userInput="Login as Admin" stepKey="commentLoginAsAdmin"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create 37 Products and Subcategory --> + <comment userInput="Create 37 Products and Subcategory" stepKey="commentCreateData"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProductOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThree"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductFour"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductFive"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductSix"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductSeven"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductEight"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductNine"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductEleven"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwelve"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirteen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductFourteen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductFifteen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductSixteen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductSeventeen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductEighteen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductNineteen"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwenty"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyThree"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyFour"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyFive"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentySix"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentySeven"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyEight"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwentyNine"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirty"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtyOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtyTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtyThree"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtyFour"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtyFive"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtySix"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductThirtySeven"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createSimpleProductTwo" stepKey="deleteProductTwo"/> + <deleteData createDataKey="createSimpleProductThree" stepKey="deleteProductThree"/> + <deleteData createDataKey="createSimpleProductFour" stepKey="deleteProductFour"/> + <deleteData createDataKey="createSimpleProductFive" stepKey="deleteProductFive"/> + <deleteData createDataKey="createSimpleProductSix" stepKey="deleteProductSix"/> + <deleteData createDataKey="createSimpleProductSeven" stepKey="deleteProductSeven"/> + <deleteData createDataKey="createSimpleProductEight" stepKey="deleteProductEight"/> + <deleteData createDataKey="createSimpleProductNine" stepKey="deleteProductNine"/> + <deleteData createDataKey="createSimpleProductTen" stepKey="deleteProductTen"/> + <deleteData createDataKey="createSimpleProductEleven" stepKey="deleteProductEleven"/> + <deleteData createDataKey="createSimpleProductTwelve" stepKey="deleteProductTwelve"/> + <deleteData createDataKey="createSimpleProductThirteen" stepKey="deleteProductThirteen"/> + <deleteData createDataKey="createSimpleProductFourteen" stepKey="deleteProductFourteen"/> + <deleteData createDataKey="createSimpleProductFifteen" stepKey="deleteProductFifteen"/> + <deleteData createDataKey="createSimpleProductSixteen" stepKey="deleteProductSixteen"/> + <deleteData createDataKey="createSimpleProductSeventeen" stepKey="deleteProductSeventeen"/> + <deleteData createDataKey="createSimpleProductEighteen" stepKey="deleteProductEighteen"/> + <deleteData createDataKey="createSimpleProductNineteen" stepKey="deleteProductNineteen"/> + <deleteData createDataKey="createSimpleProductTwenty" stepKey="deleteProductTwenty"/> + <deleteData createDataKey="createSimpleProductTwentyOne" stepKey="deleteProductTwentyOne"/> + <deleteData createDataKey="createSimpleProductTwentyTwo" stepKey="deleteProductTwentyTwo"/> + <deleteData createDataKey="createSimpleProductTwentyThree" stepKey="deleteProductTwentyThree"/> + <deleteData createDataKey="createSimpleProductTwentyFour" stepKey="deleteProductTwentyFour"/> + <deleteData createDataKey="createSimpleProductTwentyFive" stepKey="deleteProductTwentyFive"/> + <deleteData createDataKey="createSimpleProductTwentySix" stepKey="deleteProductTwentySix"/> + <deleteData createDataKey="createSimpleProductTwentySeven" stepKey="deleteProductTwentySeven"/> + <deleteData createDataKey="createSimpleProductTwentyEight" stepKey="deleteProductTwentyEight"/> + <deleteData createDataKey="createSimpleProductTwentyNine" stepKey="deleteProductTwentyNine"/> + <deleteData createDataKey="createSimpleProductThirty" stepKey="deleteProductThirty"/> + <deleteData createDataKey="createSimpleProductThirtyOne" stepKey="deleteProductThirtyOne"/> + <deleteData createDataKey="createSimpleProductThirtyTwo" stepKey="deleteProductThirtyTwo"/> + <deleteData createDataKey="createSimpleProductThirtyThree" stepKey="deleteProductThirtyThree"/> + <deleteData createDataKey="createSimpleProductThirtyFour" stepKey="deleteProductThirtyFour"/> + <deleteData createDataKey="createSimpleProductThirtyFive" stepKey="deleteProductThirtyFive"/> + <deleteData createDataKey="createSimpleProductThirtySix" stepKey="deleteProductThirtySix"/> + <deleteData createDataKey="createSimpleProductThirtySeven" stepKey="deleteProductThirtySeven"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Verify configuration for default number of products displayed in the grid view--> + <comment userInput="Verify configuration for default number of products displayed in the grid view" stepKey="commentVerifyDefaultValues"/> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigPagePage"/> + <waitForPageLoad stepKey="waitForConfigPageLoad" /> + <conditionalClick selector="{{AdminCatalogStorefrontConfigSection.sectionHeader}}" dependentSelector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" visible="false" stepKey="openCatalogConfigStorefrontSection"/> + <waitForElementVisible selector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" stepKey="waitForSectionOpen"/> + <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" userInput="12,24,36" stepKey="seeDefaultValueAllowedNumberProductsPerPage"/> + <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageDefaultValue}}" userInput="12" stepKey="seeDefaultValueProductPerPage"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Open storefront on the category page --> + <comment userInput="Open storefront on the category page" stepKey="commentOpenStorefront"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToStorefrontCreatedCategoryPage"/> + <!-- Check the drop-down at the bottom of page contains options --> + <comment userInput="Check the drop-down at the bottom of page contains options" stepKey="commentCheckOptions"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToBottomToolbarSection"/> + <assertElementContainsAttribute selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" attribute="value" expectedValue="12" stepKey="assertPerPageFirstValue" /> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="24" stepKey="selectPerPageSecondValue" /> + <assertElementContainsAttribute selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" attribute="value" expectedValue="24" stepKey="assertPerPageSecondValue" /> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="36" stepKey="selectPerPageThirdValue" /> + <assertElementContainsAttribute selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" attribute="value" expectedValue="36" stepKey="assertPerPageThirdValue" /> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml new file mode 100644 index 0000000000000..bb46f8010eaa8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml @@ -0,0 +1,52 @@ +<?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="StorefrontForthLevelCategoryTest"> + <annotations> + <features value="Catalog"/> + <stories value="Category"/> + <title value="Storefront forth level category test"/> + <description value="When the submenu was created in the third stage follow, the submenu works"/> + <severity value="MAJOR"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="SubCategoryWithParent" stepKey="category2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="category3"> + <requiredEntity createDataKey="category2"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="category4"> + <requiredEntity createDataKey="category3"/> + </createData> + </before> + <after> + <deleteData createDataKey="category4" stepKey="deleteCategory4"/> + <deleteData createDataKey="category3" stepKey="deleteCategory3"/> + <deleteData createDataKey="category2" stepKey="deleteCategory2"/> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <moveMouseOver + selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category1.name$$)}}" + stepKey="hoverCategoryLevelOne"/> + <moveMouseOver + selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category2.name$$)}}" + stepKey="hoverCategoryLevelTwo"/> + <moveMouseOver + selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category3.name$$)}}" + stepKey="hoverCategoryLevelThree"/> + <moveMouseOver + selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category4.name$$)}}" + stepKey="hoverCategoryLevelFour"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml index 386633f0e9476..21f8e2e070e32 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml @@ -41,6 +41,9 @@ </actionGroup> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByNameAndSrc(SimpleProductNameWithDoubleQuote.name, ProductImage.fileName)}}" stepKey="seeCorrectImageCategoryPage"/> @@ -88,6 +91,9 @@ <deleteData createDataKey="createCategoryOne" stepKey="deleteCategory"/> </after> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategoryOne.name$$)}}" stepKey="navigateToCategoryPage"/> <waitForPageLoad stepKey="waitforCategoryPageToLoad"/> @@ -111,11 +117,10 @@ <waitForPageLoad stepKey="waitforCategoryPageToLoad2"/> <!--Open product display page--> - <click selector="{{StorefrontCategoryProductSection.ProductTitleByNumber('1')}}" stepKey="goToProduct2DisplayPage"/> - <!--<click selector="{{StorefrontCategoryProductSection.ProductTitleByName(productWithHTMLEntityOne.name)}}" stepKey="clickProductToGoProductPage"/>--> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(productWithHTMLEntityTwo.name)}}" stepKey="clickProductToGoSecondProductPage"/> <waitForPageLoad stepKey="waitForProductDisplayPageLoad3"/> - <!--Veriy the breadcrumbs on Product Display page--> + <!--Verify the breadcrumbs on Product Display page--> <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="Home" stepKey="seeHomePageInBreadcrumbs2"/> <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$createCategoryOne.name$$" stepKey="seeCorrectBreadCrumbCategory2"/> <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$productTwo.name$$" stepKey="seeCorrectBreadCrumbProduct2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml new file mode 100644 index 0000000000000..deb6700c56990 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -0,0 +1,227 @@ +<?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="VerifyCategoryProductAndProductCategoryPartialReindexTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Categories Indexer"/> + <title value="Verify Category Product and Product Category partial reindex"/> + <description value="Verify that Merchant Developer can use console commands to perform partial reindex for Category Products, Product Categories, and Catalog Search"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11386"/> + <group value="catalog"/> + <group value="indexer"/> + </annotations> + <before> + <!-- Change "Category Products" and "Product Categories" indexers to "Update by Schedule" mode --> + <magentoCLI command="indexer:set-mode" arguments="schedule catalog_category_product catalog_product_category" stepKey="setIndexerMode"/> + + <!-- Create categories K, L, M, N with different nesting in the tree and Anchor = Yes/No--> + <!-- Category K is an anchor category --> + <createData entity="_defaultCategory" stepKey="categoryK"/> + <!-- Category L is a non-anchor subcategory of category K --> + <createData entity="SubCategoryNonAnchor" stepKey="categoryL"> + <requiredEntity createDataKey="categoryK"/> + </createData> + <!-- Category M is a subcategory of category L --> + <createData entity="SubCategoryWithParent" stepKey="categoryM"> + <requiredEntity createDataKey="categoryL"/> + </createData> + <!-- Category N is a subcategory of category K --> + <createData entity="SubCategoryWithParent" stepKey="categoryN"> + <requiredEntity createDataKey="categoryK"/> + </createData> + + <!-- Create different Products with different settings, assign to categories: --> + <!-- Product A in 0 categories, i.e. not assigned to any category --> + <createData entity="SimpleProduct2" stepKey="productA"/> + <!-- Product B in 1 category M --> + <createData entity="SimpleProduct3" stepKey="productB"> + <requiredEntity createDataKey="categoryM"/> + </createData> + <!-- Product C in 2 categories M and N --> + <createData entity="SimpleProduct2" stepKey="productC"/> + + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryNAndMToProductC"> + <argument name="productId" value="$$productC.id$$"/> + <argument name="categoryName" value="$$categoryN.name$$, $$categoryM.name$$"/> + </actionGroup> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> + </before> + <after> + <!-- Change "Category Products" and "Product Categories" indexers to "Update on Save" mode --> + <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeMode"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!-- Delete data --> + <deleteData createDataKey="productA" stepKey="deleteProductA"/> + <deleteData createDataKey="productB" stepKey="deleteProductB"/> + <deleteData createDataKey="productC" stepKey="deleteProductC"/> + <deleteData createDataKey="categoryN" stepKey="deleteCategoryN"/> + <deleteData createDataKey="categoryM" stepKey="deleteCategoryM"/> + <deleteData createDataKey="categoryL" stepKey="deleteCategoryL"/> + <deleteData createDataKey="categoryK" stepKey="deleteCategoryK"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open categories K, L, M, N on Storefront --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="onCategoryK"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="onCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="seeMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProducts"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="onCategoryM"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryN"/> + + <!-- Open Products A, B, C to edit. Assign/unassign categories to/from them. Save changes --> + <!-- Assign category K to Product A --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryK"> + <argument name="productId" value="$$productA.id$$"/> + <argument name="categoryName" value="$$categoryK.name$$"/> + </actionGroup> + + <!-- Unassign category M from Product B --> + <amOnPage url="{{AdminProductEditPage.url($$productB.id$$)}}" stepKey="amOnEditCategoryPageB"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryM"> + <argument name="categoryName" value="$$categoryM.name$$"/> + </actionGroup> + + <!-- Assign category L to Product C --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryNAndM"> + <argument name="productId" value="$$productC.id$$"/> + <argument name="categoryName" value="$$categoryL.name$$"/> + </actionGroup> + + <!-- "One or more indexers are invalid. Make sure your Magento cron job is running." global warning message appears --> + <click selector="{{AdminSystemMessagesSection.systemMessagesDropdown}}" stepKey="openMessageSection"/> + <see userInput="One or more indexers are invalid. Make sure your Magento cron job is running." selector="{{AdminSystemMessagesSection.warning}}" stepKey="seeWarningMessage"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are not applied yet --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryK"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="seeEmptyMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProduct"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryM"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductInCategoryN"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCLI command="cron:run" stepKey="runCronAgain"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryK"/> + <see userInput="$$productA.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAOnCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryKWithProductC"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryL"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLWithProductC"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryM"/> + <waitForPageLoad stepKey="waitForStorefrontCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMAndProductC"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCAndCategoryN"/> + + <!-- Open categories K, L, N to edit. Assign/unassign Products to/from them. Save changes --> + + <!-- Remove Product A assignment for category K --> + <amOnPage url="{{AdminProductEditPage.url($$productA.id$$)}}" stepKey="amOnEditProductPageA"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryK"> + <argument name="categoryName" value="$$categoryK.name$$"/> + </actionGroup> + + <!-- Remove Product C assignment for category L --> + <amOnPage url="{{AdminProductEditPage.url($$productC.id$$)}}" stepKey="amOnEditProductPageC"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryL"> + <argument name="categoryName" value="$$categoryL.name$$"/> + </actionGroup> + + <!-- Add Product B assignment for category N --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryN"> + <argument name="productId" value="$$productB.id$$"/> + <argument name="categoryName" value="$$categoryN.name$$"/> + </actionGroup> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are not applied yet --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryK"/> + <see userInput="$$productA.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAWithCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryL"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLAndProductC"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMWithProductC"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryN"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="firstCronRun"/> + <magentoCLI command="cron:run" stepKey="secondCronRun"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> + + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryK"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productBOnCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="noProductsMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductsOnCategoryL"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMPageAndProductC"/> + + <!-- Category N contains only Products B and C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryN"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBAndCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryN"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php index 2008d0b9414c5..19c578e976cdd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/InventoryTest.php @@ -85,7 +85,7 @@ protected function setUp() $this->backordersMock = $this->createMock(\Magento\CatalogInventory\Model\Source\Backorders::class); $this->stockMock = $this->createMock(\Magento\CatalogInventory\Model\Source\Stock::class); $this->coreRegistryMock = $this->createMock(\Magento\Framework\Registry::class); - $this->moduleManager = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); + $this->moduleManager = $this->createMock(\Magento\Framework\Module\Manager::class); $this->storeManagerMock = $this->getMockForAbstractClass( \Magento\Store\Model\StoreManagerInterface::class, [], diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php index 8333ed22e1da0..dcbd3161733aa 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php @@ -5,54 +5,81 @@ */ namespace Magento\Catalog\Test\Unit\Block\Widget; +use Exception; +use Magento\Catalog\Block\Widget\Link; +use Magento\Catalog\Model\ResourceModel\AbstractResource; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Url; +use Magento\Framework\Url\ModifierInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use ReflectionClass; +use RuntimeException; -class LinkTest extends \PHPUnit\Framework\TestCase +/** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LinkTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface + * @var PHPUnit_Framework_MockObject_MockObject|StoreManagerInterface */ protected $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\UrlRewrite\Model\UrlFinderInterface + * @var PHPUnit_Framework_MockObject_MockObject|UrlFinderInterface */ protected $urlFinder; /** - * @var \Magento\Catalog\Block\Widget\Link + * @var Link */ protected $block; /** - * @var \Magento\Catalog\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractResource|PHPUnit_Framework_MockObject_MockObject */ protected $entityResource; + /** + * @inheritDoc + */ protected function setUp() { - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->urlFinder = $this->createMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->urlFinder = $this->createMock(UrlFinderInterface::class); - $context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $context = $this->createMock(Context::class); $context->expects($this->any()) ->method('getStoreManager') ->will($this->returnValue($this->storeManager)); $this->entityResource = - $this->createMock(\Magento\Catalog\Model\ResourceModel\AbstractResource::class); - - $this->block = (new ObjectManager($this))->getObject(\Magento\Catalog\Block\Widget\Link::class, [ - 'context' => $context, - 'urlFinder' => $this->urlFinder, - 'entityResource' => $this->entityResource - ]); + $this->createMock(AbstractResource::class); + + $this->block = (new ObjectManager($this))->getObject( + Link::class, + [ + 'context' => $context, + 'urlFinder' => $this->urlFinder, + 'entityResource' => $this->entityResource + ] + ); } /** - * @expectedException \RuntimeException + * Tests getHref with wrong id_path + * + * @expectedException RuntimeException * @expectedExceptionMessage Parameter id_path is not set. */ public function testGetHrefWithoutSetIdPath() @@ -61,7 +88,9 @@ public function testGetHrefWithoutSetIdPath() } /** - * @expectedException \RuntimeException + * Tests getHref with wrong id_path + * + * @expectedException RuntimeException * @expectedExceptionMessage Wrong id_path structure. */ public function testGetHrefIfSetWrongIdPath() @@ -70,27 +99,30 @@ public function testGetHrefIfSetWrongIdPath() $this->block->getHref(); } + /** + * Tests getHref with wrong store ID + * + * @expectedException Exception + */ public function testGetHrefWithSetStoreId() { $this->block->setData('id_path', 'type/id'); $this->block->setData('store_id', 'store_id'); - $this->storeManager->expects($this->once()) - ->method('getStore')->with('store_id') - // interrupt test execution - ->will($this->throwException(new \Exception())); - - try { - $this->block->getHref(); - } catch (\Exception $e) { - } + ->method('getStore') + ->with('store_id') + ->will($this->throwException(new Exception())); + $this->block->getHref(); } + /** + * Tests getHref with not found URL + */ public function testGetHrefIfRewriteIsNotFound() { $this->block->setData('id_path', 'entity_type/entity_id'); - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']); + $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']); $store->expects($this->any()) ->method('getId'); @@ -105,52 +137,107 @@ public function testGetHrefIfRewriteIsNotFound() } /** - * @param string $url - * @param string $separator + * Tests getHref whether it should include the store code or not + * * @dataProvider dataProviderForTestGetHrefWithoutUrlStoreSuffix + * @param string $path + * @param int|null $storeId + * @param bool $includeStoreCode + * @param string $expected + * @throws \ReflectionException */ - public function testGetHrefWithoutUrlStoreSuffix($url, $separator) - { - $storeId = 15; - $storeCode = 'store-code'; - $requestPath = 'request-path'; + public function testStoreCodeShouldBeIncludedInURLOnlyIfItIsConfiguredSo( + string $path, + ?int $storeId, + bool $includeStoreCode, + string $expected + ) { $this->block->setData('id_path', 'entity_type/entity_id'); - - $rewrite = $this->createMock(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class); - $rewrite->expects($this->once()) - ->method('getRequestPath') - ->will($this->returnValue($requestPath)); - - $store = $this->createPartialMock( - \Magento\Store\Model\Store::class, - ['getId', 'getUrl', 'getCode', '__wakeUp'] + $this->block->setData('store_id', $storeId); + $objectManager = new ObjectManager($this); + + $rewrite = $this->createPartialMock(UrlRewrite::class, ['getRequestPath']); + $url = $this->createPartialMock(Url::class, ['setScope', 'getUrl']); + $urlModifier = $this->getMockForAbstractClass(ModifierInterface::class); + $config = $this->getMockForAbstractClass(ReinitableConfigInterface::class); + $store = $objectManager->getObject( + Store::class, + [ + 'storeManager' => $this->storeManager, + 'url' => $url, + 'config' => $config + ] ); - $store->expects($this->once()) - ->method('getId') - ->will($this->returnValue($storeId)); - $store->expects($this->once()) + $property = (new ReflectionClass(get_class($store)))->getProperty('urlModifier'); + $property->setAccessible(true); + $property->setValue($store, $urlModifier); + + $urlModifier->expects($this->any()) + ->method('execute') + ->willReturnArgument(0); + $config->expects($this->any()) + ->method('getValue') + ->willReturnMap( + [ + [Store::XML_PATH_USE_REWRITES, ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, null, true], + [ + Store::XML_PATH_STORE_IN_URL, + ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, + null, $includeStoreCode + ] + ] + ); + + $url->expects($this->any()) + ->method('setScope') + ->willReturnSelf(); + + $url->expects($this->any()) ->method('getUrl') - ->with('', ['_direct' => $requestPath]) - ->will($this->returnValue($url)); - $store->expects($this->once()) - ->method('getCode') - ->will($this->returnValue($storeCode)); + ->willReturnCallback( + function ($route, $params) use ($storeId) { + $baseUrl = rtrim($this->storeManager->getStore($storeId)->getBaseUrl(), '/'); + return $baseUrl .'/' . ltrim($params['_direct'], '/'); + } + ); - $this->storeManager->expects($this->once()) - ->method('getStore') - ->will($this->returnValue($store)); + $store->addData(['store_id' => 1, 'code' => 'french']); - $this->urlFinder->expects($this->once())->method('findOneByData') - ->with([ + $store2 = clone $store; + $store2->addData(['store_id' => 2, 'code' => 'german']); + + $this->storeManager + ->expects($this->any()) + ->method('getStore') + ->willReturnMap( + [ + [null, $store], + [1, $store], + [2, $store2], + ] + ); + + $this->urlFinder->expects($this->once()) + ->method('findOneByData') + ->with( + [ UrlRewrite::ENTITY_ID => 'entity_id', UrlRewrite::ENTITY_TYPE => 'entity_type', - UrlRewrite::STORE_ID => $storeId, - ]) + UrlRewrite::STORE_ID => $this->storeManager->getStore($storeId)->getStoreId(), + ] + ) ->will($this->returnValue($rewrite)); - $this->assertEquals($url . $separator . '___store=' . $storeCode, $this->block->getHref()); + $rewrite->expects($this->once()) + ->method('getRequestPath') + ->will($this->returnValue($path)); + + $this->assertEquals($expected, $this->block->getHref()); } + /** + * Tests getLabel with custom text + */ public function testGetLabelWithCustomText() { $customText = 'Some text'; @@ -158,6 +245,9 @@ public function testGetLabelWithCustomText() $this->assertEquals($customText, $this->block->getLabel()); } + /** + * Tests getLabel without custom text + */ public function testGetLabelWithoutCustomText() { $category = 'Some text'; @@ -178,17 +268,25 @@ public function testGetLabelWithoutCustomText() public function dataProviderForTestGetHrefWithoutUrlStoreSuffix() { return [ - ['url', '?'], - ['url?some_parameter', '&'], + ['/accessories.html', null, true, 'french/accessories.html'], + ['/accessories.html', null, false, '/accessories.html'], + ['/accessories.html', 1, true, 'french/accessories.html'], + ['/accessories.html', 1, false, '/accessories.html'], + ['/accessories.html', 2, true, 'german/accessories.html'], + ['/accessories.html', 2, false, '/accessories.html?___store=german'], + ['/accessories.html?___store=german', 2, false, '/accessories.html?___store=german'], ]; } + /** + * Tests getHref with product entity and additional category id in the id_path + */ public function testGetHrefWithForProductWithCategoryIdParameter() { $storeId = 15; $this->block->setData('id_path', ProductUrlRewriteGenerator::ENTITY_TYPE . '/entity_id/category_id'); - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']); + $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']); $store->expects($this->any()) ->method('getId') ->will($this->returnValue($storeId)); @@ -197,13 +295,16 @@ public function testGetHrefWithForProductWithCategoryIdParameter() ->method('getStore') ->will($this->returnValue($store)); - $this->urlFinder->expects($this->once())->method('findOneByData') - ->with([ - UrlRewrite::ENTITY_ID => 'entity_id', - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId, - UrlRewrite::METADATA => ['category_id' => 'category_id'], - ]) + $this->urlFinder->expects($this->once()) + ->method('findOneByData') + ->with( + [ + UrlRewrite::ENTITY_ID => 'entity_id', + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeId, + UrlRewrite::METADATA => ['category_id' => 'category_id'], + ] + ) ->will($this->returnValue(false)); $this->block->getHref(); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/EditTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/EditTest.php index cd8d57a51d609..7d4fa86cd6901 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/EditTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/EditTest.php @@ -96,7 +96,9 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->categoryMock = $this->createPartialMock(\Magento\Catalog\Model\Category::class, [ + $this->categoryMock = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + [ 'getPath', 'addData', 'getId', @@ -104,9 +106,12 @@ protected function setUp() 'getResource', 'setStoreId', 'toArray' - ]); + ] + ); - $this->contextMock = $this->createPartialMock(\Magento\Backend\App\Action\Context::class, [ + $this->contextMock = $this->createPartialMock( + \Magento\Backend\App\Action\Context::class, + [ 'getTitle', 'getRequest', 'getObjectManager', @@ -115,7 +120,9 @@ protected function setUp() 'getMessageManager', 'getResultRedirectFactory', 'getSession' - ]); + ] + ); + $this->resultRedirectFactoryMock = $this->createPartialMock( \Magento\Backend\Model\View\Result\RedirectFactory::class, ['create'] @@ -285,44 +292,8 @@ public function dataProviderExecute() */ private function mockInitCategoryCall() { - /** - * @var \Magento\Framework\Registry - * |\PHPUnit_Framework_MockObject_MockObject $registryMock - */ - $registryMock = $this->createPartialMock(\Magento\Framework\Registry::class, ['register']); - /** - * @var \Magento\Cms\Model\Wysiwyg\Config - * |\PHPUnit_Framework_MockObject_MockObject $wysiwygConfigMock - */ - $wysiwygConfigMock = $this->createPartialMock(\Magento\Cms\Model\Wysiwyg\Config::class, ['setStoreId']); - /** - * @var \Magento\Store\Model\StoreManagerInterface - * |\PHPUnit_Framework_MockObject_MockObject $storeManagerMock - */ - $storeManagerMock = $this->getMockForAbstractClass( - \Magento\Store\Model\StoreManagerInterface::class, - [], - '', - false, - true, - true, - ['getStore', 'getRootCategoryId'] - ); - $this->objectManagerMock->expects($this->atLeastOnce()) ->method('create') ->will($this->returnValue($this->categoryMock)); - - $this->objectManagerMock->expects($this->atLeastOnce()) - ->method('get') - ->will( - $this->returnValueMap( - [ - [\Magento\Framework\Registry::class, $registryMock], - [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwygConfigMock], - [\Magento\Store\Model\StoreManagerInterface::class, $storeManagerMock], - ] - ) - ); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php index 746d3340a6605..da58943bb3722 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php @@ -134,13 +134,10 @@ public function testExecuteWithGenericException() ->willReturn($categoryMock); $this->objectManager->expects($this->any()) ->method('get') - ->withConsecutive([Registry::class], [Registry::class], [\Magento\Cms\Model\Wysiwyg\Config::class]) ->willReturnMap([[Registry::class, $registry], [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwygConfig]]); $categoryMock->expects($this->once()) ->method('move') - ->willThrowException(new \Exception( - __('Some exception') - )); + ->willThrowException(new \Exception(__('Some exception'))); $this->messageManager->expects($this->once()) ->method('addErrorMessage') ->with(__('There was a category move error.')); @@ -208,7 +205,6 @@ public function testExecuteWithLocalizedException() ->willReturn($categoryMock); $this->objectManager->expects($this->any()) ->method('get') - ->withConsecutive([Registry::class], [Registry::class], [\Magento\Cms\Model\Wysiwyg\Config::class]) ->willReturnMap([[Registry::class, $registry], [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwygConfig]]); $this->messageManager->expects($this->once()) ->method('addExceptionMessage'); @@ -236,9 +232,7 @@ public function testExecuteWithLocalizedException() ->willReturn(true); $categoryMock->expects($this->once()) ->method('move') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException( - __($exceptionMessage) - )); + ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__($exceptionMessage))); $this->resultJsonFactoryMock ->expects($this->once()) ->method('create') @@ -280,7 +274,6 @@ public function testSuccessfulCategorySave() ->willReturn($categoryMock); $this->objectManager->expects($this->any()) ->method('get') - ->withConsecutive([Registry::class], [Registry::class], [\Magento\Cms\Model\Wysiwyg\Config::class]) ->willReturnMap([[Registry::class, $registry], [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwygConfig]]); $this->messageManager->expects($this->once()) ->method('getMessages') diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index 742148b1bf7f1..856f8a20b9daf 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -136,18 +136,22 @@ public function testExecute() $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ + ->willReturnMap( + [ ['frontend_label', null, 'test_frontend_label'], ['attribute_code', null, 'test_attribute_code'], ['new_attribute_set_name', null, 'test_attribute_set_name'], ['serialized_options', '[]', $serializedOptions], - ]); + ] + ); $this->objectManagerMock->expects($this->exactly(2)) ->method('create') - ->willReturnMap([ + ->willReturnMap( + [ [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] - ]); + ] + ); $this->attributeMock->expects($this->once()) ->method('loadByCode') ->willReturnSelf(); @@ -182,9 +186,9 @@ public function testExecute() /** * @dataProvider provideUniqueData - * @param array $options - * @param boolean $isError - * @throws \Magento\Framework\Exception\NotFoundException + * @param array $options + * @param boolean $isError + * @throws \Magento\Framework\Exception\NotFoundException */ public function testUniqueValidation(array $options, $isError) { @@ -192,13 +196,15 @@ public function testUniqueValidation(array $options, $isError) $countFunctionCalls = ($isError) ? 6 : 5; $this->requestMock->expects($this->exactly($countFunctionCalls)) ->method('getParam') - ->willReturnMap([ + ->willReturnMap( + [ ['frontend_label', null, null], ['attribute_code', null, "test_attribute_code"], ['new_attribute_set_name', null, 'test_attribute_set_name'], ['message_key', null, Validate::DEFAULT_MESSAGE_KEY], ['serialized_options', '[]', $serializedOptions], - ]); + ] + ); $this->formDataSerializerMock ->expects($this->once()) @@ -323,22 +329,24 @@ public function provideUniqueData() * Check that empty admin scope labels will trigger error. * * @dataProvider provideEmptyOption - * @param array $options - * @throws \Magento\Framework\Exception\NotFoundException + * @param array $options + * @throws \Magento\Framework\Exception\NotFoundException */ public function testEmptyOption(array $options, $result) { $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ + ->willReturnMap( + [ ['frontend_label', null, null], ['frontend_input', 'select', 'multipleselect'], ['attribute_code', null, "test_attribute_code"], ['new_attribute_set_name', null, 'test_attribute_set_name'], ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ['serialized_options', '[]', $serializedOptions], - ]); + ] + ); $this->formDataSerializerMock ->expects($this->once()) @@ -439,6 +447,129 @@ public function provideEmptyOption() ]; } + /** + * Check that admin scope labels which only contain spaces will trigger error. + * + * @dataProvider provideWhitespaceOption + * @param array $options + * @param $result + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function testWhitespaceOption(array $options, $result) + { + $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['frontend_label', null, null], + ['frontend_input', 'select', 'multipleselect'], + ['attribute_code', null, "test_attribute_code"], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], + ] + ); + + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn($options); + + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); + + $this->attributeMock->expects($this->once()) + ->method('loadByCode') + ->willReturnSelf(); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->willReturnArgument(0); + + $response = $this->getModel()->execute(); + $responseObject = json_decode($response); + $this->assertEquals($responseObject, $result); + } + + /** + * Dataprovider for testWhitespaceOption. + * + * @return array + */ + public function provideWhitespaceOption() + { + return [ + 'whitespace admin scope options' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => [' '], + ], + ], + ], + (object) [ + 'error' => true, + 'message' => 'The value of Admin scope can\'t be empty.', + ] + ], + 'not empty admin scope options' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => ['asdads'], + ], + ], + ], + (object) [ + 'error' => false, + ] + ], + 'whitespace admin scope options and deleted' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => [' '], + ], + 'delete' => [ + 'option_0' => '1', + ], + ], + ], + (object) [ + 'error' => false, + ], + ], + 'whitespace admin scope options and not deleted' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => [' '], + ], + 'delete' => [ + 'option_0' => '0', + ], + ], + ], + (object) [ + 'error' => true, + 'message' => 'The value of Admin scope can\'t be empty.', + ], + ], + ]; + } + /** * @throws \Magento\Framework\Exception\NotFoundException */ @@ -449,13 +580,15 @@ public function testExecuteWithOptionsDataError() . "If the error persists, please try again later."; $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ + ->willReturnMap( + [ ['frontend_label', null, 'test_frontend_label'], ['attribute_code', null, 'test_attribute_code'], ['new_attribute_set_name', null, 'test_attribute_set_name'], ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ['serialized_options', '[]', $serializedOptions], - ]); + ] + ); $this->formDataSerializerMock ->expects($this->once()) @@ -465,10 +598,12 @@ public function testExecuteWithOptionsDataError() $this->objectManagerMock ->method('create') - ->willReturnMap([ + ->willReturnMap( + [ [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] - ]); + ] + ); $this->attributeCodeValidatorMock ->method('isValid') @@ -485,10 +620,14 @@ public function testExecuteWithOptionsDataError() ->willReturn($this->resultJson); $this->resultJson->expects($this->once()) ->method('setJsonData') - ->with(json_encode([ - 'error' => true, - 'message' => $message - ])) + ->with( + json_encode( + [ + 'error' => true, + 'message' => $message + ] + ) + ) ->willReturnSelf(); $this->getModel()->execute(); @@ -498,23 +637,25 @@ public function testExecuteWithOptionsDataError() * Test execute with an invalid attribute code * * @dataProvider provideInvalidAttributeCodes - * @param string $attributeCode - * @param $result - * @throws \Magento\Framework\Exception\NotFoundException + * @param string $attributeCode + * @param $result + * @throws \Magento\Framework\Exception\NotFoundException */ public function testExecuteWithInvalidAttributeCode($attributeCode, $result) { $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ + ->willReturnMap( + [ ['frontend_label', null, null], ['frontend_input', 'select', 'multipleselect'], ['attribute_code', null, $attributeCode], ['new_attribute_set_name', null, 'test_attribute_set_name'], ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], ['serialized_options', '[]', $serializedOptions], - ]); + ] + ); $this->formDataSerializerMock ->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index c889c58e3df3a..134c6f9edeaf7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\ProductRepositoryInterface as ProductRepository; @@ -11,6 +12,8 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Locale\Format; +use Magento\Framework\Locale\FormatInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Model\StoreManagerInterface; @@ -100,6 +103,11 @@ class HelperTest extends \PHPUnit\Framework\TestCase */ private $dateTimeFilterMock; + /** + * @var FormatInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -152,6 +160,10 @@ protected function setUp() ->setMethods(['prepareProductAttributes']) ->disableOriginalConstructor() ->getMock(); + $this->localeFormatMock = $this->getMockBuilder(Format::class) + ->setMethods(['getNumber']) + ->disableOriginalConstructor() + ->getMock(); $this->helper = $this->objectManager->getObject( Helper::class, @@ -164,7 +176,8 @@ protected function setUp() 'productLinkFactory' => $this->productLinkFactoryMock, 'productRepository' => $this->productRepositoryMock, 'linkTypeProvider' => $this->linkTypeProviderMock, - 'attributeFilter' => $this->attributeFilterMock + 'attributeFilter' => $this->attributeFilterMock, + 'localeFormat' => $this->localeFormatMock, ] ); @@ -207,9 +220,9 @@ public function testInitialize( ->willReturn($this->assembleLinkTypes($linkTypes)); $optionsData = [ - 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1', 'option_id' => ''], - 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'], - 'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14'] + 'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => '1', 'option_id' => ''], + 'option2' => ['is_delete' => false, 'name' => 'name2', 'price' => '2', 'option_id' => '13'], + 'option3' => ['is_delete' => false, 'name' => 'name3', 'price' => '3', 'option_id' => '14'], ]; $specialFromDate = '2018-03-03 19:30:00'; $productData = [ @@ -252,7 +265,7 @@ public function testInitialize( $this->requestMock->expects($this->any())->method('getPost')->willReturnMap( [ ['product', [], $productData], - ['use_default', null, $useDefaults] + ['use_default', null, $useDefaults], ] ); $this->linkResolverMock->expects($this->once())->method('getLinks')->willReturn($links); @@ -276,30 +289,38 @@ public function testInitialize( $secondExpectedCustomOption->setData($optionsData['option3']); $this->customOptionFactoryMock->expects($this->any()) ->method('create') - ->willReturnMap([ + ->willReturnMap( [ - ['data' => $optionsData['option2']], - $firstExpectedCustomOption - ], [ - ['data' => $optionsData['option3']], - $secondExpectedCustomOption + [ + ['data' => $optionsData['option2']], + $firstExpectedCustomOption, + ], + [ + ['data' => $optionsData['option3']], + $secondExpectedCustomOption, + ], ] - ]); + ); $website = $this->getMockBuilder(WebsiteInterface::class)->getMockForAbstractClass(); $website->expects($this->any())->method('getId')->willReturn(1); $this->storeManagerMock->expects($this->once())->method('isSingleStoreMode')->willReturn($isSingleStore); $this->storeManagerMock->expects($this->any())->method('getWebsite')->willReturn($website); + $this->localeFormatMock->expects($this->any()) + ->method('getNumber') + ->willReturnArgument(0); $this->assembleProductRepositoryMock($links); $this->productLinkFactoryMock->expects($this->any()) ->method('create') - ->willReturnCallback(function () { - return $this->getMockBuilder(ProductLink::class) - ->setMethods(null) - ->disableOriginalConstructor() - ->getMock(); - }); + ->willReturnCallback( + function () { + return $this->getMockBuilder(ProductLink::class) + ->setMethods(null) + ->disableOriginalConstructor() + ->getMock(); + } + ); $this->attributeFilterMock->expects($this->any())->method('prepareProductAttributes')->willReturnArgument(1); @@ -388,8 +409,8 @@ public function initializeDataProvider() 'price' => 1.00, 'position' => 1, 'record_id' => 1, - ] - ] + ], + ], ], 'linkTypes' => ['related', 'upsell', 'crosssell'], 'expected_links' => [ @@ -533,16 +554,16 @@ public function mergeProductOptionsDataProvider() [ 'option_type_id' => '2', 'key1' => 'val1', - 'default_key1' => 'val2' - ] - ] - ] + 'default_key1' => 'val2', + ], + ], + ], ], [ 4 => [ 'key1' => '1', - 'values' => [3 => ['key1' => 1]] - ] + 'values' => [3 => ['key1' => 1]], + ], ], [ [ @@ -553,11 +574,11 @@ public function mergeProductOptionsDataProvider() [ 'option_type_id' => '2', 'key1' => 'val1', - 'default_key1' => 'val2' - ] - ] - ] - ] + 'default_key1' => 'val2', + ], + ], + ], + ], ], 'key2 is replaced, key1 is not (checkbox is not checked)' => [ [ @@ -573,17 +594,17 @@ public function mergeProductOptionsDataProvider() 'key1' => 'val1', 'key2' => 'val2', 'default_key1' => 'val11', - 'default_key2' => 'val22' - ] - ] - ] + 'default_key2' => 'val22', + ], + ], + ], ], [ 5 => [ 'key1' => '0', 'title' => '1', - 'values' => [2 => ['key1' => 1]] - ] + 'values' => [2 => ['key1' => 1]], + ], ], [ [ @@ -599,11 +620,11 @@ public function mergeProductOptionsDataProvider() 'key1' => 'val11', 'key2' => 'val2', 'default_key1' => 'val11', - 'default_key2' => 'val22' - ] - ] - ] - ] + 'default_key2' => 'val22', + ], + ], + ], + ], ], 'key1 is replaced, key2 has no default value' => [ [ @@ -618,17 +639,17 @@ public function mergeProductOptionsDataProvider() 'key1' => 'val1', 'title' => 'val2', 'default_key1' => 'val11', - 'default_title' => 'val22' - ] - ] - ] + 'default_title' => 'val22', + ], + ], + ], ], [ 7 => [ 'key1' => '1', 'key2' => '1', - 'values' => [2 => ['key1' => 0, 'title' => 1]] - ] + 'values' => [2 => ['key1' => 0, 'title' => 1]], + ], ], [ [ @@ -643,10 +664,10 @@ public function mergeProductOptionsDataProvider() 'title' => 'val22', 'default_key1' => 'val11', 'default_title' => 'val22', - 'is_delete_store_title' => 1 - ] - ] - ] + 'is_delete_store_title' => 1, + ], + ], + ], ], ], ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php index 359e43070ba72..df96eff852d4e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -110,14 +109,15 @@ protected function setUp() 'resultFactory' => $resultFactory ]; /** @var \Magento\Backend\App\Action\Context $context */ - $context = $this->initContext($additionalParams, [[Action::class, $this->actionMock]]); + $context = $this->initContext($additionalParams); $this->action = new \Magento\Catalog\Controller\Adminhtml\Product\MassStatus( $context, $this->productBuilderMock, $this->priceProcessorMock, $this->filterMock, - $collectionFactoryMock + $collectionFactoryMock, + $this->actionMock ); } @@ -137,11 +137,13 @@ public function testMassStatusAction() ->willReturn([3]); $this->request->expects($this->exactly(3)) ->method('getParam') - ->willReturnMap([ - ['store', null, $storeId], - ['status', null, $status], - ['filters', [], $filters] - ]); + ->willReturnMap( + [ + ['store', null, $storeId], + ['status', null, $status], + ['filters', [], $filters] + ] + ); $this->actionMock->expects($this->once()) ->method('updateAttributes') ->with([3], ['status' => $status], 2); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php index 60c6f2f1bd821..b826f25d7c591 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php @@ -25,7 +25,7 @@ class ViewTest extends \PHPUnit\Framework\TestCase protected $response; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Category|\PHPUnit_Framework_MockObject_MockObject */ protected $categoryHelper; @@ -165,13 +165,17 @@ protected function setUp() ->method('create') ->will($this->returnValue($this->page)); - $this->action = (new ObjectManager($this))->getObject(\Magento\Catalog\Controller\Category\View::class, [ - 'context' => $this->context, - 'catalogDesign' => $this->catalogDesign, - 'categoryRepository' => $this->categoryRepository, - 'storeManager' => $this->storeManager, - 'resultPageFactory' => $resultPageFactory - ]); + $this->action = (new ObjectManager($this))->getObject( + \Magento\Catalog\Controller\Category\View::class, + [ + 'context' => $this->context, + 'catalogDesign' => $this->catalogDesign, + 'categoryRepository' => $this->categoryRepository, + 'storeManager' => $this->storeManager, + 'resultPageFactory' => $resultPageFactory, + 'categoryHelper' => $this->categoryHelper + ] + ); } public function testApplyCustomLayoutUpdate() @@ -179,19 +183,17 @@ public function testApplyCustomLayoutUpdate() $categoryId = 123; $pageLayout = 'page_layout'; - $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ - [\Magento\Catalog\Helper\Category::class, $this->categoryHelper], - ])); - - $this->request->expects($this->any())->method('getParam')->will($this->returnValueMap([ - [Action::PARAM_NAME_URL_ENCODED], - ['id', false, $categoryId], - ])); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + [Action::PARAM_NAME_URL_ENCODED], + ['id', false, $categoryId] + ] + ); $this->categoryRepository->expects($this->any())->method('get')->with($categoryId) ->will($this->returnValue($this->category)); - $this->categoryHelper->expects($this->any())->method('canShow')->will($this->returnValue(true)); + $this->categoryHelper->expects($this->once())->method('canShow')->with($this->category)->willReturn(true); $settings = $this->createPartialMock( \Magento\Framework\DataObject::class, diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index 2759371dc96e7..f37192538655e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Helper; use Magento\Catalog\Helper\Image; @@ -29,6 +30,11 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ protected $assetRepository; + /** + * @var \Magento\Framework\Config\View|\PHPUnit\Framework\MockObject\MockObject + */ + protected $configView; + /** * @var \Magento\Framework\View\ConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -58,6 +64,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->configView = $this->getMockBuilder(\Magento\Framework\Config\View::class) + ->disableOriginalConstructor() + ->getMock(); + $this->viewConfig = $this->getMockBuilder(\Magento\Framework\View\ConfigInterface::class) ->getMockForAbstractClass(); @@ -151,23 +161,89 @@ public function initDataProvider() ]; } + /** + * @param array $data - optional 'frame' key + * @param bool $whiteBorders view config + * @param bool $expectedKeepFrame + * @dataProvider initKeepFrameDataProvider + */ + public function testInitKeepFrame($data, $whiteBorders, $expectedKeepFrame) + { + $imageId = 'test_image_id'; + $attributes = []; + + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->prepareAttributes($data, $imageId); + + $this->configView->expects(isset($data['frame']) ? $this->never() : $this->once()) + ->method('getVarValue') + ->with('Magento_Catalog', 'product_image_white_borders') + ->willReturn($whiteBorders); + + $this->viewConfig->expects($this->once()) + ->method('getViewConfig') + ->willReturn($this->configView); + + $this->image->expects($this->once()) + ->method('setKeepFrame') + ->with($expectedKeepFrame) + ->willReturnSelf(); + + $this->helper->init($productMock, $imageId, $attributes); + } + + /** + * @return array + */ + public function initKeepFrameDataProvider() + { + return [ + // when frame defined explicitly, it wins + [ + 'mediaImage' => [ + 'frame' => 1, + ], + 'whiteBorders' => true, + 'expected' => true, + ], + [ + 'mediaImage' => [ + 'frame' => 0, + ], + 'whiteBorders' => true, + 'expected' => false, + ], + // when frame is not defined, var is used + [ + 'mediaImage' => [], + 'whiteBorders' => true, + 'expected' => true, + ], + [ + 'mediaImage' => [], + 'whiteBorders' => false, + 'expected' => false, + ], + ]; + } + /** * @param $data * @param $imageId */ protected function prepareAttributes($data, $imageId) { - $configViewMock = $this->getMockBuilder(\Magento\Framework\Config\View::class) - ->disableOriginalConstructor() - ->getMock(); - $configViewMock->expects($this->once()) + $this->configView->expects($this->once()) ->method('getMediaAttributes') ->with('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId) ->willReturn($data); $this->viewConfig->expects($this->once()) ->method('getViewConfig') - ->willReturn($configViewMock); + ->willReturn($this->configView); } /** @@ -220,32 +296,34 @@ protected function prepareWatermarkProperties($data) { $this->scopeConfig->expects($this->any()) ->method('getValue') - ->willReturnMap([ + ->willReturnMap( [ - 'design/watermark/' . $data['type'] . '_image', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - $data['watermark'] - ], - [ - 'design/watermark/' . $data['type'] . '_imageOpacity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - $data['watermark_opacity'] - ], - [ - 'design/watermark/' . $data['type'] . '_position', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - $data['watermark_position'] - ], - [ - 'design/watermark/' . $data['type'] . '_size', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - $data['watermark_size'] - ], - ]); + [ + 'design/watermark/' . $data['type'] . '_image', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null, + $data['watermark'] + ], + [ + 'design/watermark/' . $data['type'] . '_imageOpacity', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null, + $data['watermark_opacity'] + ], + [ + 'design/watermark/' . $data['type'] . '_position', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null, + $data['watermark_position'] + ], + [ + 'design/watermark/' . $data['type'] . '_size', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + null, + $data['watermark_size'] + ], + ] + ); $this->image->expects($this->any()) ->method('setWatermarkFile') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/CustomlayoutupdateTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/CustomlayoutupdateTest.php deleted file mode 100644 index 01fad60609c29..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/CustomlayoutupdateTest.php +++ /dev/null @@ -1,137 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend; - -use Magento\Framework\DataObject; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -class CustomlayoutupdateTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var string - */ - private $attributeName = 'private'; - - /** - * @var \Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate - */ - private $model; - - /** - * @expectedException \Magento\Eav\Model\Entity\Attribute\Exception - */ - public function testValidateException() - { - $object = new DataObject(); - $object->setData($this->attributeName, 'exception'); - $this->model->validate($object); - } - - /** - * @param string - * @dataProvider validateProvider - */ - public function testValidate($data) - { - $object = new DataObject(); - $object->setData($this->attributeName, $data); - - $this->assertTrue($this->model->validate($object)); - $this->assertTrue($this->model->validate($object)); - } - - /** - * @return array - */ - public function validateProvider() - { - return [[''], ['xml']]; - } - - protected function setUp() - { - $helper = new ObjectManager($this); - $this->model = $helper->getObject( - \Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate::class, - [ - 'layoutUpdateValidatorFactory' => $this->getMockedLayoutUpdateValidatorFactory() - ] - ); - $this->model->setAttribute($this->getMockedAttribute()); - } - - /** - * @return \Magento\Framework\View\Model\Layout\Update\ValidatorFactory - */ - private function getMockedLayoutUpdateValidatorFactory() - { - $mockBuilder = $this->getMockBuilder(\Magento\Framework\View\Model\Layout\Update\ValidatorFactory::class); - $mockBuilder->disableOriginalConstructor(); - $mockBuilder->setMethods(['create']); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('create') - ->will($this->returnValue($this->getMockedValidator())); - - return $mock; - } - - /** - * @return \Magento\Framework\View\Model\Layout\Update\Validator - */ - private function getMockedValidator() - { - $mockBuilder = $this->getMockBuilder(\Magento\Framework\View\Model\Layout\Update\Validator::class); - $mockBuilder->disableOriginalConstructor(); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('isValid') - ->will( - /** - * @param string $xml - * $return bool - */ - $this->returnCallback( - function ($xml) { - if ($xml == 'exception') { - return false; - } else { - return true; - } - } - ) - ); - - $mock->expects($this->any()) - ->method('getMessages') - ->will($this->returnValue(['error'])); - - return $mock; - } - - /** - * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute - */ - private function getMockedAttribute() - { - $mockBuilder = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class); - $mockBuilder->disableOriginalConstructor(); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('getName') - ->will($this->returnValue($this->attributeName)); - - $mock->expects($this->any()) - ->method('getIsRequired') - ->will($this->returnValue(false)); - - return $mock; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php index cce00c50d37af..fde793d5c5f89 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php @@ -108,6 +108,7 @@ public function testExecute(): void ]; $linkField = 'entity_id'; $productId = 10; + $originalProductId = 11; /** @var \PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) @@ -124,7 +125,7 @@ public function testExecute(): void ->willReturnMap( [ ['tier_price', $originalTierPrices], - ['entity_id', $productId] + ['entity_id', $originalProductId] ] ); $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php index 8f3aa66e57c5e..4c3450d555f1d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php @@ -17,6 +17,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Ui\DataProvider\Modifier\PoolInterface; +use Magento\Framework\Stdlib\ArrayUtils; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -78,6 +79,14 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ private $modifierPool; + /** + * @var ArrayUtils|\PHPUnit_Framework_MockObject_MockObject + */ + private $arrayUtils; + + /** + * @inheritDoc + */ protected function setUp() { $this->eavValidationRules = $this->getMockBuilder(EavValidationRules::class) @@ -128,6 +137,10 @@ protected function setUp() ->getMock(); $this->modifierPool = $this->getMockBuilder(PoolInterface::class)->getMockForAbstractClass(); + + $this->arrayUtils = $this->getMockBuilder(ArrayUtils::class) + ->setMethods(['flatten']) + ->disableOriginalConstructor()->getMock(); } /** @@ -157,7 +170,8 @@ private function getModel() 'eavConfig' => $this->eavConfig, 'request' => $this->request, 'categoryFactory' => $this->categoryFactory, - 'pool' => $this->modifierPool + 'pool' => $this->modifierPool, + 'arrayUtils' => $this->arrayUtils ] ); @@ -206,10 +220,12 @@ public function testGetDataNoFileExists() ->getMock(); $categoryMock->expects($this->exactly(2)) ->method('getData') - ->willReturnMap([ - ['', null, $categoryData], - ['image', null, $categoryData['image']], - ]); + ->willReturnMap( + [ + ['', null, $categoryData], + ['image', null, $categoryData['image']], + ] + ); $categoryMock->expects($this->any()) ->method('getExistsStoreValueFlag') ->with('url_key') @@ -280,10 +296,12 @@ public function testGetData() ->getMock(); $categoryMock->expects($this->exactly(2)) ->method('getData') - ->willReturnMap([ - ['', null, $categoryData], - ['image', null, $categoryData['image']], - ]); + ->willReturnMap( + [ + ['', null, $categoryData], + ['image', null, $categoryData['image']], + ] + ); $categoryMock->expects($this->any()) ->method('getExistsStoreValueFlag') ->with('url_key') @@ -331,10 +349,12 @@ public function testGetData() public function testGetMetaWithoutParentInheritanceResolving() { + $this->arrayUtils->expects($this->atLeastOnce())->method('flatten')->willReturn([1,3,3]); + $categoryMock = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor() ->getMock(); - $this->registry->expects($this->once()) + $this->registry->expects($this->atLeastOnce()) ->method('registry') ->with('category') ->willReturn($categoryMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php index 71f5ca33d1303..6977a9ad1c7cc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php @@ -191,6 +191,9 @@ public function testIsExist($fileName, $fileMediaPath) $this->assertTrue($this->model->isExist($fileName)); } + /** + * @return array + */ public function isExistProvider() { return [ @@ -213,6 +216,9 @@ public function testIsBeginsWithMediaDirectoryPath($fileName, $expected) $this->assertEquals($expected, $this->model->isBeginsWithMediaDirectoryPath($fileName)); } + /** + * @return array + */ public function isBeginsWithMediaDirectoryPathProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php index f0e17c7938b27..09fbdf293ffc9 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php @@ -95,11 +95,11 @@ public function testGetCollection() ); $expectedResult = [ - 0 => ['name' => 'Product Four', 'position' => 0], - 1 => ['name' => 'Product Five', 'position' => 0], - 2 => ['name' => 'Product Three', 'position' => 2], - 3 => ['name' => 'Product Two', 'position' => 2], - 4 => ['name' => 'Product One', 'position' => 10], + 0 => ['name' => 'Product Four', 'position' => 0, 'link_type' => 'crosssell'], + 1 => ['name' => 'Product Five', 'position' => 0, 'link_type' => 'crosssell'], + 2 => ['name' => 'Product Three', 'position' => 2, 'link_type' => 'crosssell'], + 3 => ['name' => 'Product Two', 'position' => 2, 'link_type' => 'crosssell'], + 4 => ['name' => 'Product One', 'position' => 10, 'link_type' => 'crosssell'], ]; $actualResult = $this->model->getCollection($this->productMock, 'crosssell'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/ProductTest.php index 7a7c11e95d9b7..f7dde23e1510e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/ProductTest.php @@ -84,15 +84,13 @@ public function testExecuteWithIndexerWorking() { $ids = [1, 2, 3]; - $this->indexerMock->expects($this->once())->method('isWorking')->will($this->returnValue(true)); $this->prepareIndexer(); $rowMock = $this->createPartialMock( \Magento\Catalog\Model\Indexer\Category\Product\Action\Rows::class, ['execute'] ); - $rowMock->expects($this->at(0))->method('execute')->with($ids, true)->will($this->returnSelf()); - $rowMock->expects($this->at(1))->method('execute')->with($ids, false)->will($this->returnSelf()); + $rowMock->expects($this->at(0))->method('execute')->with($ids)->will($this->returnSelf()); $this->rowsMock->expects($this->once())->method('create')->will($this->returnValue($rowMock)); @@ -103,14 +101,13 @@ public function testExecuteWithIndexerNotWorking() { $ids = [1, 2, 3]; - $this->indexerMock->expects($this->once())->method('isWorking')->will($this->returnValue(false)); $this->prepareIndexer(); $rowMock = $this->createPartialMock( \Magento\Catalog\Model\Indexer\Category\Product\Action\Rows::class, ['execute'] ); - $rowMock->expects($this->once())->method('execute')->with($ids, false)->will($this->returnSelf()); + $rowMock->expects($this->once())->method('execute')->with($ids)->will($this->returnSelf()); $this->rowsMock->expects($this->once())->method('create')->will($this->returnValue($rowMock)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/CategoryTest.php index 74f748ef9bf00..4e6659f85f5df 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/CategoryTest.php @@ -84,15 +84,13 @@ public function testExecuteWithIndexerWorking() { $ids = [1, 2, 3]; - $this->indexerMock->expects($this->once())->method('isWorking')->will($this->returnValue(true)); $this->prepareIndexer(); $rowMock = $this->createPartialMock( \Magento\Catalog\Model\Indexer\Product\Category\Action\Rows::class, ['execute'] ); - $rowMock->expects($this->at(0))->method('execute')->with($ids, true)->will($this->returnSelf()); - $rowMock->expects($this->at(1))->method('execute')->with($ids, false)->will($this->returnSelf()); + $rowMock->expects($this->at(0))->method('execute')->with($ids)->will($this->returnSelf()); $this->rowsMock->expects($this->once())->method('create')->will($this->returnValue($rowMock)); @@ -103,14 +101,13 @@ public function testExecuteWithIndexerNotWorking() { $ids = [1, 2, 3]; - $this->indexerMock->expects($this->once())->method('isWorking')->will($this->returnValue(false)); $this->prepareIndexer(); $rowMock = $this->createPartialMock( \Magento\Catalog\Model\Indexer\Product\Category\Action\Rows::class, ['execute'] ); - $rowMock->expects($this->once())->method('execute')->with($ids, false)->will($this->returnSelf()); + $rowMock->expects($this->once())->method('execute')->with($ids)->will($this->returnSelf()); $this->rowsMock->expects($this->once())->method('create')->will($this->returnValue($rowMock)); @@ -123,7 +120,7 @@ public function testExecuteWithIndexerNotWorking() protected function prepareIndexer() { - $this->indexerRegistryMock->expects($this->once()) + $this->indexerRegistryMock->expects($this->any()) ->method('get') ->with(\Magento\Catalog\Model\Indexer\Product\Category::INDEXER_ID) ->will($this->returnValue($this->indexerMock)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php index 8ca23df31cdee..c59aa1988be55 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php @@ -178,6 +178,7 @@ public function validateFilterDataProvider() ['filter' => '0', 'result' => false], ['filter' => 0, 'result' => false], ['filter' => '100500INF', 'result' => false], + ['filter' => '-10\'[0]', 'result' => false], ]; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php index 8733f305ce091..731c5efd99746 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Test\Unit\Model\Layer; @@ -72,9 +73,13 @@ public function testGetFilters($method, $value, $expectedClass) $this->objectManagerMock->expects($this->at(1)) ->method('create') - ->with($expectedClass, [ - 'data' => ['attribute_model' => $this->attributeMock], - 'layer' => $this->layerMock]) + ->with( + $expectedClass, + [ + 'data' => ['attribute_model' => $this->attributeMock], + 'layer' => $this->layerMock + ] + ) ->will($this->returnValue('filter')); $this->attributeMock->expects($this->once()) @@ -95,8 +100,8 @@ public function getFiltersDataProvider() { return [ [ - 'method' => 'getAttributeCode', - 'value' => FilterList::PRICE_FILTER, + 'method' => 'getFrontendInput', + 'value' => 'price', 'expectedClass' => 'PriceFilterClass', ], [ @@ -105,8 +110,8 @@ public function getFiltersDataProvider() 'expectedClass' => 'DecimalFilterClass', ], [ - 'method' => 'getAttributeCode', - 'value' => null, + 'method' => 'getFrontendInput', + 'value' => 'text', 'expectedClass' => 'AttributeFilterClass', ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php index 80b6db2a516bd..809fa0225278c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Test\Unit\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Copier; @@ -46,6 +47,11 @@ class CopierTest extends \PHPUnit\Framework\TestCase */ protected $metadata; + /** + * @var ScopeOverriddenValue|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeOverriddenValue; + protected function setUp() { $this->copyConstructorMock = $this->createMock(\Magento\Catalog\Model\Product\CopyConstructorInterface::class); @@ -59,6 +65,7 @@ protected function setUp() $this->optionRepositoryMock; $this->productMock = $this->createMock(Product::class); $this->productMock->expects($this->any())->method('getEntityId')->willReturn(1); + $this->scopeOverriddenValue = $this->createMock(ScopeOverriddenValue::class); $this->metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) ->disableOriginalConstructor() @@ -67,15 +74,20 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $metadataPool->expects($this->any())->method('getMetadata')->willReturn($this->metadata); + $this->_model = new Copier( $this->copyConstructorMock, - $this->productFactoryMock + $this->productFactoryMock, + $this->scopeOverriddenValue ); - $this->setProperties($this->_model, [ - 'optionRepository' => $this->optionRepositoryMock, - 'metadataPool' => $metadataPool, - ]); + $this->setProperties( + $this->_model, + [ + 'optionRepository' => $this->optionRepositoryMock, + 'metadataPool' => $metadataPool, + ] + ); } /** @@ -103,10 +115,12 @@ public function testCopy() ]; $this->productMock->expects($this->atLeastOnce())->method('getWebsiteIds'); $this->productMock->expects($this->atLeastOnce())->method('getCategoryIds'); - $this->productMock->expects($this->any())->method('getData')->willReturnMap([ - ['', null, $productData], - ['linkField', null, '1'], - ]); + $this->productMock->expects($this->any())->method('getData')->willReturnMap( + [ + ['', null, $productData], + ['linkField', null, '1'], + ] + ); $entityMock = $this->getMockForAbstractClass( \Magento\Eav\Model\Entity\AbstractEntity::class, @@ -191,9 +205,11 @@ public function testCopy() $this->metadata->expects($this->any())->method('getLinkField')->willReturn('linkField'); - $duplicateMock->expects($this->any())->method('getData')->willReturnMap([ - ['linkField', null, '2'], - ]); + $duplicateMock->expects($this->any())->method('getData')->willReturnMap( + [ + ['linkField', null, '2'], + ] + ); $this->optionRepositoryMock->expects($this->once()) ->method('duplicate') ->with($this->productMock, $duplicateMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php index 1d12645019d1e..6d4e98b60ad18 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php @@ -7,6 +7,9 @@ namespace Magento\Catalog\Test\Unit\Model\Product\Gallery; +/** + * Tests for \Magento\Catalog\Model\Product\Gallery\GalleryManagement. + */ class GalleryManagementTest extends \PHPUnit\Framework\TestCase { /** @@ -39,11 +42,16 @@ class GalleryManagementTest extends \PHPUnit\Framework\TestCase */ protected $attributeValueMock; + /** + * @inheritdoc + */ protected function setUp() { $this->productRepositoryMock = $this->createMock(\Magento\Catalog\Api\ProductRepositoryInterface::class); $this->contentValidatorMock = $this->createMock(\Magento\Framework\Api\ImageContentValidatorInterface::class); - $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + $this->productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ 'setStoreId', 'getData', 'getStoreId', @@ -51,7 +59,8 @@ protected function setUp() 'getCustomAttribute', 'getMediaGalleryEntries', 'setMediaGalleryEntries', - ]); + ] + ); $this->mediaGalleryEntryMock = $this->createMock(\Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface::class); $this->model = new \Magento\Catalog\Model\Product\Gallery\GalleryManagement( @@ -151,6 +160,8 @@ public function testUpdateWithNonExistingImage() $existingEntryMock->expects($this->once())->method('getId')->willReturn(43); $this->productMock->expects($this->once())->method('getMediaGalleryEntries') ->willReturn([$existingEntryMock]); + $existingEntryMock->expects($this->once())->method('getTypes')->willReturn([]); + $entryMock->expects($this->once())->method('getTypes')->willReturn([]); $entryMock->expects($this->once())->method('getId')->willReturn($entryId); $this->model->update($productSku, $entryMock); } @@ -172,12 +183,19 @@ public function testUpdateWithCannotSaveException() $existingEntryMock->expects($this->once())->method('getId')->willReturn($entryId); $this->productMock->expects($this->once())->method('getMediaGalleryEntries') ->willReturn([$existingEntryMock]); + $existingEntryMock->expects($this->once())->method('getTypes')->willReturn([]); + $entryMock->expects($this->once())->method('getTypes')->willReturn([]); $entryMock->expects($this->once())->method('getId')->willReturn($entryId); $this->productRepositoryMock->expects($this->once())->method('save')->with($this->productMock) ->willThrowException(new \Exception()); $this->model->update($productSku, $entryMock); } + /** + * Check update gallery entry behavior. + * + * @return void + */ public function testUpdate() { $productSku = 'testProduct'; @@ -203,14 +221,13 @@ public function testUpdate() ->willReturn([$existingEntryMock, $existingSecondEntryMock]); $entryMock->expects($this->exactly(2))->method('getId')->willReturn($entryId); - $entryMock->expects($this->once())->method('getFile')->willReturn("base64"); - $entryMock->expects($this->once())->method('setId')->with(null); - $entryMock->expects($this->exactly(2))->method('getTypes')->willReturn(['image']); + $entryMock->expects($this->once())->method('getTypes')->willReturn(['image']); $this->productMock->expects($this->once())->method('setMediaGalleryEntries') ->with([$entryMock, $existingSecondEntryMock]) ->willReturnSelf(); $this->productRepositoryMock->expects($this->once())->method('save')->with($this->productMock); + $this->assertTrue($this->model->update($productSku, $entryMock)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php new file mode 100644 index 0000000000000..22e3a88574e03 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\Image; + +use Magento\Catalog\Model\Product\Image; +use Magento\Catalog\Model\Product\Image\ParamsBuilder; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Config\View; +use Magento\Framework\View\ConfigInterface; +use Magento\Store\Model\ScopeInterface; + +class ParamsBuilderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ConfigInterface + */ + private $viewConfig; + + /** + * @var ParamsBuilder + */ + private $model; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->viewConfig = $this->createMock(ConfigInterface::class); + $this->model = $objectManager->getObject( + ParamsBuilder::class, + [ + 'scopeConfig' => $this->scopeConfig, + 'viewConfig' => $this->viewConfig, + ] + ); + } + + /** + * Test watermark location. + */ + public function testWatermarkLocation() + { + $imageArguments = [ + 'type' => 'type', + 'height' => 'image_height', + 'width' => 'image_width', + 'angle' => 'angle', + 'background' => [1, 2, 3] + ]; + $scopeId = 1; + $quality = 100; + $file = 'file'; + $width = 'width'; + $height = 'height'; + $size = "{$width}x{$height}"; + $opacity = 'opacity'; + $position = 'position'; + + $viewMock = $this->createMock(View::class); + $viewMock->expects($this->once()) + ->method('getVarValue') + ->with('Magento_Catalog', 'product_image_white_borders') + ->willReturn(true); + + $this->viewConfig->expects($this->once()) + ->method('getViewConfig') + ->with(['area' => Area::AREA_FRONTEND]) + ->willReturn($viewMock); + + $this->scopeConfig->expects($this->exactly(5))->method('getValue')->withConsecutive( + [ + Image::XML_PATH_JPEG_QUALITY + ], + [ + "design/watermark/{$imageArguments['type']}_image", + ScopeInterface::SCOPE_STORE, + $scopeId, + ], + [ + "design/watermark/{$imageArguments['type']}_size", + ScopeInterface::SCOPE_STORE], + [ + "design/watermark/{$imageArguments['type']}_imageOpacity", + ScopeInterface::SCOPE_STORE, + $scopeId + ], + [ + "design/watermark/{$imageArguments['type']}_position", + ScopeInterface::SCOPE_STORE, + $scopeId + ] + )->willReturnOnConsecutiveCalls( + $quality, + $file, + $size, + $opacity, + $position + ); + + $actual = $this->model->build($imageArguments, $scopeId); + $expected = [ + 'image_type' => $imageArguments['type'], + 'background' => $imageArguments['background'], + 'angle' => $imageArguments['angle'], + 'quality' => $quality, + 'keep_aspect_ratio' => true, + 'keep_frame' => true, + 'keep_transparency' => true, + 'constrain_only' => true, + 'watermark_file' => $file, + 'watermark_image_opacity' => $opacity, + 'watermark_position' => $position, + 'watermark_width' => $width, + 'watermark_height' => $height, + 'image_height' => $imageArguments['height'], + 'image_width' => $imageArguments['width'], + ]; + + $this->assertEquals( + $expected, + $actual + ); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php index 99151d1c8dd39..cdbef1bec3872 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php @@ -165,9 +165,13 @@ public function testTierPrices($priceScope, $expectedWebsiteId) $this->websiteMock->expects($this->any())->method('getId')->will($this->returnValue($expectedWebsiteId)); $this->tpFactory->expects($this->any()) ->method('create') - ->will($this->returnCallback(function () { - return $this->objectManagerHelper->getObject(\Magento\Catalog\Model\Product\TierPrice::class); - })); + ->will( + $this->returnCallback( + function () { + return $this->objectManagerHelper->getObject(\Magento\Catalog\Model\Product\TierPrice::class); + } + ) + ); // create sample TierPrice objects that would be coming from a REST call $tierPriceExtensionMock = $this->getMockBuilder(ProductTierPriceExtensionInterface::class) @@ -198,9 +202,10 @@ public function testTierPrices($priceScope, $expectedWebsiteId) $tpArray = $this->product->getData($this::KEY_TIER_PRICE); $this->assertNotNull($tpArray); $this->assertTrue(is_array($tpArray)); - $this->assertEquals(sizeof($tps), sizeof($tpArray)); + $this->assertEquals(count($tps), count($tpArray)); - for ($i = 0; $i < sizeof($tps); $i++) { + $count = count($tps); + for ($i = 0; $i < $count; $i++) { $tpData = $tpArray[$i]; $this->assertEquals($expectedWebsiteId, $tpData['website_id'], 'Website Id does not match'); $this->assertEquals($tps[$i]->getValue(), $tpData['price'], 'Price/Value does not match'); @@ -226,12 +231,13 @@ public function testTierPrices($priceScope, $expectedWebsiteId) $tpRests = $this->model->getTierPrices($this->product); $this->assertNotNull($tpRests); $this->assertTrue(is_array($tpRests)); - $this->assertEquals(sizeof($tps), sizeof($tpRests)); + $this->assertEquals(count($tps), count($tpRests)); foreach ($tpRests as $tpRest) { $this->assertEquals(50, $tpRest->getExtensionAttributes()->getPercentageValue()); } - for ($i = 0; $i < sizeof($tps); $i++) { + $count = count($tps); + for ($i = 0; $i < $count; $i++) { $this->assertEquals( $tps[$i]->getValue(), $tpRests[$i]->getValue(), diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 8bf8473080c54..ce234e17c41aa 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -13,6 +13,7 @@ use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\StoreManagerInterface; /** * Product Test @@ -40,7 +41,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManager; @@ -207,6 +208,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ private $eavConfig; + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -215,7 +221,7 @@ protected function setUp() $this->categoryIndexerMock = $this->getMockForAbstractClass(\Magento\Framework\Indexer\IndexerInterface::class); $this->moduleManager = $this->createPartialMock( - \Magento\Framework\Module\ModuleManagerInterface::class, + \Magento\Framework\Module\Manager::class, ['isEnabled'] ); $this->extensionAttributes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) @@ -303,13 +309,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $storeManager->expects($this->any()) + $this->storeManager->expects($this->any()) ->method('getStore') ->will($this->returnValue($this->store)); - $storeManager->expects($this->any()) + $this->storeManager->expects($this->any()) ->method('getWebsite') ->will($this->returnValue($this->website)); $this->indexerRegistryMock = $this->createPartialMock( @@ -394,7 +400,7 @@ protected function setUp() 'extensionFactory' => $this->extensionAttributesFactory, 'productPriceIndexerProcessor' => $this->productPriceProcessor, 'catalogProductOptionFactory' => $optionFactory, - 'storeManager' => $storeManager, + 'storeManager' => $this->storeManager, 'resource' => $this->resource, 'registry' => $this->registry, 'moduleManager' => $this->moduleManager, @@ -450,6 +456,51 @@ public function testGetStoreIds() $this->assertEquals($expectedStoreIds, $this->model->getStoreIds()); } + /** + * @dataProvider getSingleStoreIds + * @param bool $isObjectNew + */ + public function testGetStoreSingleSiteModelIds( + bool $isObjectNew + ) { + $websiteIDs = [0 => 2]; + $this->model->setWebsiteIds( + !$isObjectNew ? $websiteIDs : array_flip($websiteIDs) + ); + + $this->model->isObjectNew($isObjectNew); + + $this->storeManager->expects( + $this->exactly( + (int) !$isObjectNew + ) + ) + ->method('isSingleStoreMode') + ->will($this->returnValue(true)); + + $this->website->expects( + $this->once() + )->method('getStoreIds') + ->will($this->returnValue($websiteIDs)); + + $this->assertEquals($websiteIDs, $this->model->getStoreIds()); + } + + /** + * @return array + */ + public function getSingleStoreIds() + { + return [ + [ + false + ], + [ + true + ], + ]; + } + public function testGetStoreId() { $this->model->setStoreId(3); @@ -1026,6 +1077,12 @@ public function testGetProductLinks() $outputRelatedLink->setPosition(0); $expectedOutput = [$outputRelatedLink]; $this->productLinkRepositoryMock->expects($this->once())->method('getList')->willReturn($expectedOutput); + $typeInstance = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + ->setMethods(['getSku']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $typeInstance->method('getSku')->willReturn('model'); + $this->productTypeInstanceMock->method('factory')->willReturn($typeInstance); $links = $this->model->getProductLinks(); $this->assertEquals($links, $expectedOutput); } @@ -1221,8 +1278,7 @@ public function testGetMediaGalleryImagesMerging() { $mediaEntries = [ - 'images' => - [ + 'images' => [ [ 'value_id' => 1, 'file' => 'imageFile.jpg', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 6370a4a7a27e2..0316b2e374d2f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -98,7 +98,7 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getStore', 'getId', 'getWebsiteId']) ->getMockForAbstractClass(); - $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $moduleManager = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); $catalogProductFlatState = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Flat\State::class) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php index 596148b627506..a29e76c5c8ff1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Link/Product/CollectionTest.php @@ -7,6 +7,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -105,11 +107,11 @@ protected function setUp() $this->storeManagerMock ->expects($this->any()) ->method('getStore') - ->will($this->returnCallback( + ->willReturnCallback( function ($store) { return is_object($store) ? $store : new \Magento\Framework\DataObject(['id' => 42]); } - )); + ); $this->catalogHelperMock = $this->createMock(\Magento\Catalog\Helper\Data::class); $this->stateMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\State::class); $this->scopeConfigInterfaceMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -125,6 +127,11 @@ function ($store) { $productLimitationFactoryMock->method('create') ->willReturn($this->createMock(ProductLimitation::class)); + $metadataMock = $this->getMockForAbstractClass(EntityMetadataInterface::class); + $metadataMock->method('getLinkField')->willReturn('entity_id'); + $metadataPoolMock = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock(); + $metadataPoolMock->method('getMetadata')->willReturn($metadataMock); + $this->collection = $this->objectManager->getObject( \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection::class, [ @@ -147,6 +154,7 @@ function ($store) { 'customerSession' => $this->sessionMock, 'dateTime' => $this->dateTimeMock, 'productLimitationFactory' => $productLimitationFactoryMock, + 'metadataPool' => $metadataPoolMock ] ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php index eb7b70c8a1718..6832d5b3399d7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Catalog\Model\View\Asset\Image; +use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Asset\ContextInterface; @@ -103,9 +104,10 @@ public function testGetContext() /** * @param string $filePath * @param array $miscParams + * @param string $readableParams * @dataProvider getPathDataProvider */ - public function testGetPath($filePath, $miscParams) + public function testGetPath($filePath, $miscParams, $readableParams) { $imageModel = $this->objectManager->getObject( Image::class, @@ -118,11 +120,13 @@ public function testGetPath($filePath, $miscParams) 'miscParams' => $miscParams ] ); - $miscParams['background'] = isset($miscParams['background']) ? implode(',', $miscParams['background']) : ''; $absolutePath = '/var/www/html/magento2ce/pub/media/catalog/product'; - $hashPath = md5(implode('_', $miscParams)); + $hashPath = 'somehash'; $this->context->method('getPath')->willReturn($absolutePath); - $this->encryptor->method('hash')->willReturn($hashPath); + $this->encryptor->expects(static::once()) + ->method('hash') + ->with($readableParams, $this->anything()) + ->willReturn($hashPath); static::assertEquals( $absolutePath . '/cache/'. $hashPath . $filePath, $imageModel->getPath() @@ -132,9 +136,10 @@ public function testGetPath($filePath, $miscParams) /** * @param string $filePath * @param array $miscParams + * @param string $readableParams * @dataProvider getPathDataProvider */ - public function testGetUrl($filePath, $miscParams) + public function testGetUrl($filePath, $miscParams, $readableParams) { $imageModel = $this->objectManager->getObject( Image::class, @@ -147,11 +152,13 @@ public function testGetUrl($filePath, $miscParams) 'miscParams' => $miscParams ] ); - $miscParams['background'] = isset($miscParams['background']) ? implode(',', $miscParams['background']) : ''; $absolutePath = 'http://localhost/pub/media/catalog/product'; - $hashPath = md5(implode('_', $miscParams)); + $hashPath = 'somehash'; $this->context->expects(static::once())->method('getBaseUrl')->willReturn($absolutePath); - $this->encryptor->expects(static::once())->method('hash')->willReturn($hashPath); + $this->encryptor->expects(static::once()) + ->method('hash') + ->with($readableParams, $this->anything()) + ->willReturn($hashPath); static::assertEquals( $absolutePath . '/cache/' . $hashPath . $filePath, $imageModel->getUrl() @@ -166,7 +173,8 @@ public function getPathDataProvider() return [ [ '/some_file.png', - [], //default value for miscParams + [], //default value for miscParams, + 'h:empty_w:empty_q:empty_r:empty_nonproportional_noframe_notransparency_notconstrainonly_nobackground', ], [ '/some_file_2.png', @@ -174,15 +182,32 @@ public function getPathDataProvider() 'image_type' => 'thumbnail', 'image_height' => 75, 'image_width' => 75, - 'keep_aspect_ratio' => 'proportional', - 'keep_frame' => 'frame', - 'keep_transparency' => 'transparency', - 'constrain_only' => 'doconstrainonly', + 'keep_aspect_ratio' => true, + 'keep_frame' => true, + 'keep_transparency' => true, + 'constrain_only' => true, 'background' => [233,1,0], 'angle' => null, 'quality' => 80, ], - ] + 'h:75_w:75_proportional_frame_transparency_doconstrainonly_rgb233,1,0_r:empty_q:80', + ], + [ + '/some_file_3.png', + [ + 'image_type' => 'thumbnail', + 'image_height' => 75, + 'image_width' => 75, + 'keep_aspect_ratio' => false, + 'keep_frame' => false, + 'keep_transparency' => false, + 'constrain_only' => false, + 'background' => [233,1,0], + 'angle' => 90, + 'quality' => 80, + ], + 'h:75_w:75_nonproportional_noframe_notransparency_notconstrainonly_rgb233,1,0_r:90_q:80', + ], ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php index 774edcfeb6b64..55acfff6d87d3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -67,9 +67,10 @@ protected function setUp(): void $this->uiComponentFactory->method('create') ->willReturn($this->column); - $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ - 'componentFactory' => $this->uiComponentFactory - ]); + $this->columnFactory = $this->objectManager->getObject( + ColumnFactory::class, + ['componentFactory' => $this->uiComponentFactory] + ); } /** @@ -111,6 +112,7 @@ public function testCreateWithNotFilterableInGridAttribute(array $filterModifier 'visible' => null, 'filter' => $filter, 'component' => 'Magento_Ui/js/grid/columns/column', + '__disableTmpl' => ['label' => true] ], ], 'context' => $this->context, diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php index dcd50d4739d70..c704d9f89581d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php @@ -12,6 +12,9 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\UiComponent\ContextInterface; +/** + * MassAction test + */ class MassActionTest extends \PHPUnit\Framework\TestCase { /** @@ -103,7 +106,8 @@ public function getPrepareDataProvider() : array [ 'type' => 'first_action', 'label' => 'First Action', - 'url' => '/module/controller/firstAction' + 'url' => '/module/controller/firstAction', + '__disableTmpl' => true ], ], [ @@ -122,7 +126,8 @@ public function getPrepareDataProvider() : array 'label' => 'Second Sub Action 2', 'url' => '/module/controller/secondSubAction2' ], - ] + ], + '__disableTmpl' => true ], ], [ @@ -141,7 +146,8 @@ public function getPrepareDataProvider() : array 'label' => 'Second Sub Action 2', 'url' => '/module/controller/disable' ], - ] + ], + '__disableTmpl' => true ], ], [ @@ -160,7 +166,8 @@ public function getPrepareDataProvider() : array 'label' => 'Second Sub Action 2', 'url' => '/module/controller/disable' ], - ] + ], + '__disableTmpl' => true ], false, false @@ -170,7 +177,8 @@ public function getPrepareDataProvider() : array [ 'type' => 'delete', 'label' => 'First Action', - 'url' => '/module/controller/delete' + 'url' => '/module/controller/delete', + '__disableTmpl' => true ], ], [ @@ -178,7 +186,8 @@ public function getPrepareDataProvider() : array [ 'type' => 'delete', 'label' => 'First Action', - 'url' => '/module/controller/delete' + 'url' => '/module/controller/delete', + '__disableTmpl' => true ], false, false @@ -188,7 +197,8 @@ public function getPrepareDataProvider() : array [ 'type' => 'delete', 'label' => 'First Action', - 'url' => '/module/controller/attributes' + 'url' => '/module/controller/attributes', + '__disableTmpl' => true ], ], [ @@ -196,7 +206,8 @@ public function getPrepareDataProvider() : array [ 'type' => 'delete', 'label' => 'First Action', - 'url' => '/module/controller/attributes' + 'url' => '/module/controller/attributes', + '__disableTmpl' => true ], false, false diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php index e9f9349100f15..e455ad47ee626 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedPricingTest.php @@ -11,7 +11,7 @@ use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Directory\Helper\Data as DirectoryHelper; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 932b09f7df9cb..bceafee0f82a4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -10,7 +10,6 @@ use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; -use Magento\Framework\App\CacheInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; @@ -161,7 +160,14 @@ public function testModifyMetaLocked($locked) ->willReturnArgument(2); $modifyMeta = $this->createModel()->modifyMeta($meta); - $this->assertEquals($locked, $modifyMeta['arguments']['data']['config']['disabled']); + $this->assertEquals( + $locked, + $modifyMeta['children']['category_ids']['arguments']['data']['config']['disabled'] + ); + $this->assertEquals( + $locked, + $modifyMeta['children']['create_category_button']['arguments']['data']['config']['disabled'] + ); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 88075b13f1430..e4c8414ce07b4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -562,6 +562,7 @@ public function setupAttributeMetaDataProvider() 'scopeLabel' => '', 'globalScope' => false, 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ], 'default_null_prod_not_new_locked_and_required' => [ @@ -581,6 +582,7 @@ public function setupAttributeMetaDataProvider() 'scopeLabel' => '', 'globalScope' => false, 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], 'locked' => true, ], @@ -601,6 +603,7 @@ public function setupAttributeMetaDataProvider() 'scopeLabel' => '', 'globalScope' => false, 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ], 'default_null_prod_new_and_not_required' => [ @@ -620,6 +623,7 @@ public function setupAttributeMetaDataProvider() 'scopeLabel' => '', 'globalScope' => false, 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ], 'default_null_prod_new_locked_and_not_required' => [ @@ -639,6 +643,7 @@ public function setupAttributeMetaDataProvider() 'scopeLabel' => '', 'globalScope' => false, 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], 'locked' => true, ], @@ -659,6 +664,7 @@ public function setupAttributeMetaDataProvider() 'scopeLabel' => '', 'globalScope' => false, 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php index a9d717db7b7f9..9d0e7fc57ffce 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php @@ -33,7 +33,7 @@ protected function setUp() parent::setUp(); $this->attributeRepositoryMock = $this->getMockBuilder(AttributeRepositoryInterface::class) - ->getMockForAbstractClass(); + ->getMockForAbstractClass(); $arrayManager = $this->objectManager->getObject(ArrayManager::class); @@ -52,10 +52,13 @@ protected function setUp() */ protected function createModel() { - return $this->objectManager->getObject(General::class, [ + return $this->objectManager->getObject( + General::class, + [ 'locator' => $this->locatorMock, 'arrayManager' => $this->arrayManagerMock, - ]); + ] + ); } public function testModifyMeta() @@ -63,8 +66,10 @@ public function testModifyMeta() $this->arrayManagerMock->expects($this->any()) ->method('merge') ->willReturnArgument(2); - $this->assertNotEmpty($this->getModel()->modifyMeta([ - 'first_panel_code' => [ + $this->assertNotEmpty( + $this->getModel()->modifyMeta( + [ + 'first_panel_code' => [ 'arguments' => [ 'data' => [ 'config' => [ @@ -72,15 +77,17 @@ public function testModifyMeta() ] ], ] - ] - ])); + ] + ] + ) + ); } /** - * @param array $data - * @param int $defaultStatusValue - * @param array $expectedResult - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @param array $data + * @param int $defaultStatusValue + * @param array $expectedResult + * @throws \Magento\Framework\Exception\NoSuchEntityException * @dataProvider modifyDataDataProvider */ public function testModifyDataNewProduct(array $data, int $defaultStatusValue, array $expectedResult) @@ -100,6 +107,97 @@ public function testModifyDataNewProduct(array $data, int $defaultStatusValue, a $this->assertSame($expectedResult, $this->generalModifier->modifyData($data)); } + /** + * Verify the product attribute status set owhen editing existing product + * + * @param array $data + * @param string $modelId + * @param int $defaultStatus + * @param int $statusAttributeValue + * @param array $expectedResult + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @dataProvider modifyDataOfExistingProductDataProvider + */ + public function testModifyDataOfExistingProduct( + array $data, + string $modelId, + int $defaultStatus, + int $statusAttributeValue, + array $expectedResult + ) { + $attributeMock = $this->getMockForAbstractClass(AttributeInterface::class); + $attributeMock->expects($this->any()) + ->method('getDefaultValue') + ->willReturn($defaultStatus); + $this->attributeRepositoryMock->expects($this->any()) + ->method('get') + ->with( + ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::CODE_STATUS + ) + ->willReturn($attributeMock); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn($modelId); + $this->productMock->expects($this->any()) + ->method('getStatus') + ->willReturn($statusAttributeValue); + $this->assertSame($expectedResult, current($this->generalModifier->modifyData($data))); + } + + /** + * @return array + */ + public function modifyDataOfExistingProductDataProvider(): array + { + return [ + 'With enable status value' => [ + 'data' => [], + 'modelId' => '1', + 'defaultStatus' => 1, + 'statusAttributeValue' => 1, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 1, + ], + ], + ], + 'Without disable status value' => [ + 'data' => [], + 'modelId' => '1', + 'defaultStatus' => 1, + 'statusAttributeValue' => 2, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 2, + ], + ], + ], + 'With enable status value with empty modelId' => [ + 'data' => [], + 'modelId' => '', + 'defaultStatus' => 1, + 'statusAttributeValue' => 1, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 1, + ], + ], + ], + 'Without disable status value with empty modelId' => [ + 'data' => [], + 'modelId' => '', + 'defaultStatus' => 2, + 'statusAttributeValue' => 2, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 2, + ], + ], + ], + ]; + } + /** * @return array */ diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index 9a6a22fcb0985..638f225ca1b82 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -74,6 +74,7 @@ public function create($attribute, $context, array $config = []) 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) ? $this->getFilterType($attribute->getFrontendInput()) : null, + '__disableTmpl' => ['label' => true], ], $config ); diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php index 0c4efa87c1a32..596b0f4118599 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php @@ -60,6 +60,7 @@ public function prepareDataSource(array $dataSource) ), 'label' => __('Edit'), 'hidden' => false, + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php b/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php index 894e2b701b5ac..f770f6b9c497f 100644 --- a/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php +++ b/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php @@ -12,6 +12,9 @@ use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\AbstractComponent; +/** + * Class MassAction + */ class MassAction extends AbstractComponent { const NAME = 'massaction'; @@ -40,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function prepare() : void { @@ -49,7 +52,8 @@ public function prepare() : void foreach ($this->getChildComponents() as $actionComponent) { $actionType = $actionComponent->getConfiguration()['type']; if ($this->isActionAllowed($actionType)) { - $config['actions'][] = $actionComponent->getConfiguration(); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $config['actions'][] = array_merge($actionComponent->getConfiguration(), ['__disableTmpl' => true]); } } $origConfig = $this->getConfiguration(); @@ -64,7 +68,7 @@ public function prepare() : void } /** - * {@inheritdoc} + * @inheritdoc */ public function getComponentName() : string { diff --git a/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php b/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php index be73940237db4..932fe4ef33d83 100644 --- a/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php +++ b/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php @@ -10,6 +10,9 @@ use Magento\Framework\UrlInterface; +/** + * Returns configuration for product Url Input type + */ class Product implements \Magento\Ui\Model\UrlInput\ConfigInterface { /** @@ -27,7 +30,7 @@ public function __construct(UrlInterface $urlBuilder) } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig(): array { @@ -46,6 +49,7 @@ public function getConfig(): array 'template' => 'ui/grid/filters/elements/ui-select', 'searchUrl' => $this->urlBuilder->getUrl('catalog/product/search'), 'filterPlaceholder' => __('Product Name or SKU'), + 'filterRateLimitMethod' => 'notifyWhenChangesStop', 'isDisplayEmptyPlaceholder' => true, 'emptyOptionsHtml' => __('Start typing to find products'), 'missingValuePlaceholder' => __('Product with ID: %s doesn\'t exist'), diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 9ad75b5fda923..f19f5abd0b423 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -14,7 +14,7 @@ use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Ui\Component\Container; use Magento\Ui\Component\Form\Element\DataType\Number; use Magento\Ui\Component\Form\Element\DataType\Price; @@ -149,6 +149,7 @@ public function modifyMeta(array $meta) $this->specialPriceDataToInline(); $this->customizeTierPrice(); + $this->customizePrice(); if (isset($this->meta['advanced-pricing'])) { $this->addAdvancedPriceLink(); @@ -197,6 +198,29 @@ protected function preparePriceFields($fieldCode) return $this; } + /** + * Customize price field. + * + * @return $this + */ + private function customizePrice(): AdvancedPricing + { + $pathFrom = $this->arrayManager->findPath('price', $this->meta, null, 'children'); + + if ($pathFrom) { + $this->meta = $this->arrayManager->merge( + $this->arrayManager->slicePath($pathFrom, 0, -2) . '/arguments/data/config', + $this->meta, + [ + 'label' => false, + 'required' => false, + ] + ); + } + + return $this; + } + /** * Customize tier price field * @@ -573,12 +597,11 @@ private function specialPriceDataToInline() $this->arrayManager->slicePath($pathFrom, 0, -2) . '/arguments/data/config', $this->meta, [ - 'label' => __('Special Price From'), + 'label' => false, + 'required' => false, 'additionalClasses' => 'admin__control-grouped-date', 'breakLine' => false, 'component' => 'Magento_Ui/js/form/components/group', - 'scopeLabel' => - $this->arrayManager->get($pathFrom . '/arguments/data/config/scopeLabel', $this->meta), ] ); $this->meta = $this->arrayManager->merge( @@ -586,8 +609,9 @@ private function specialPriceDataToInline() $this->meta, [ 'label' => __('Special Price From'), - 'scopeLabel' => null, - 'additionalClasses' => 'admin__field-date' + 'scopeLabel' => + $this->arrayManager->get($pathFrom . '/arguments/data/config/scopeLabel', $this->meta), + 'additionalClasses' => 'admin__field-date', ] ); $this->meta = $this->arrayManager->merge( diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php index 0733d21bf47d7..53c9595b59e76 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php @@ -78,11 +78,20 @@ public function getOptions() \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::SORT_ORDER_ASC ); - return $collection->getData(); + $collectionData = $collection->getData() ?? []; + + array_walk( + $collectionData, + function (&$attribute) { + $attribute['__disableTmpl'] = true; + } + ); + + return $collectionData; } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -116,17 +125,20 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyData(array $data) { - return array_replace_recursive($data, [ - $this->locator->getProduct()->getId() => [ - self::DATA_SOURCE_DEFAULT => [ - 'attribute_set_id' => $this->locator->getProduct()->getAttributeSetId() - ], + return array_replace_recursive( + $data, + [ + $this->locator->getProduct()->getId() => [ + self::DATA_SOURCE_DEFAULT => [ + 'attribute_set_id' => $this->locator->getProduct()->getAttributeSetId() + ], + ] ] - ]); + ); } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 5f1907344ce83..cd1f8e8e3379b 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -23,7 +23,6 @@ * Data provider for categories field of product page * * @api - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 101.0.0 */ @@ -120,7 +119,7 @@ public function __construct( * @return CacheInterface * @deprecated 101.0.3 */ - private function getCacheManager() + private function getCacheManager(): CacheInterface { if (!$this->cacheManager) { $this->cacheManager = ObjectManager::getInstance() @@ -148,9 +147,9 @@ public function modifyMeta(array $meta) * * @return bool */ - private function isAllowed() + private function isAllowed(): bool { - return $this->authorization->isAllowed('Magento_Catalog::categories'); + return (bool) $this->authorization->isAllowed('Magento_Catalog::categories'); } /** @@ -234,6 +233,7 @@ protected function customizeCategoriesField(array $meta) $fieldCode = 'category_ids'; $elementPath = $this->arrayManager->findPath($fieldCode, $meta, null, 'children'); $containerPath = $this->arrayManager->findPath(static::CONTAINER_PREFIX . $fieldCode, $meta, null, 'children'); + $fieldIsDisabled = $this->locator->getProduct()->isLockedAttribute($fieldCode); if (!$elementPath) { return $meta; @@ -243,13 +243,13 @@ protected function customizeCategoriesField(array $meta) 'arguments' => [ 'data' => [ 'config' => [ - 'label' => __('Categories'), + 'label' => false, + 'required' => false, 'dataScope' => '', 'breakLine' => false, 'formElement' => 'container', 'componentType' => 'container', 'component' => 'Magento_Ui/js/form/components/group', - 'scopeLabel' => __('[GLOBAL]'), 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ], ], @@ -266,6 +266,7 @@ protected function customizeCategoriesField(array $meta) 'chipsEnabled' => true, 'disableLabel' => true, 'levelsVisibility' => '1', + 'disabled' => $fieldIsDisabled, 'elementTmpl' => 'ui/grid/filters/elements/ui-select', 'options' => $this->getCategoriesTree(), 'listens' => [ @@ -291,6 +292,7 @@ protected function customizeCategoriesField(array $meta) 'formElement' => 'container', 'additionalClasses' => 'admin__field-small', 'componentType' => 'container', + 'disabled' => $fieldIsDisabled, 'component' => 'Magento_Ui/js/form/components/button', 'template' => 'ui/form/components/button/container', 'actions' => [ @@ -320,11 +322,7 @@ protected function customizeCategoriesField(array $meta) ] ]; } - $meta = $this->arrayManager->merge( - $containerPath, - $meta, - $value - ); + $meta = $this->arrayManager->merge($containerPath, $meta, $value); return $meta; } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 5d1e853cef3d1..bb743cda489fe 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -18,6 +18,7 @@ use Magento\Eav\Api\Data\AttributeGroupInterface; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrderBuilder; @@ -46,6 +47,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) * @since 101.0.0 */ class Eav extends AbstractModifier @@ -684,13 +686,20 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC 'scopeLabel' => $this->getScopeLabel($attribute), 'globalScope' => $this->isScopeGlobal($attribute), 'sortOrder' => $sortOrder * self::SORT_ORDER_MULTIPLIER, + '__disableTmpl' => ['label' => true, 'code' => true] ] ); + $product = $this->locator->getProduct(); // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { - $options = $attributeModel->getSource()->getAllOptions(true, true); + $source = $attributeModel->getSource(); + if ($source instanceof SpecificSourceInterface) { + $options = $source->getOptionsFor($product); + } else { + $options = $source->getAllOptions(true, true); + } foreach ($options as &$option) { $option['__disableTmpl'] = true; } @@ -717,7 +726,6 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC $meta = $this->arrayManager->merge($configPath, $meta, ['componentType' => Field::NAME]); } - $product = $this->locator->getProduct(); if (in_array($attributeCode, $this->attributesToDisable) || $product->isLockedAttribute($attributeCode)) { $meta = $this->arrayManager->merge($configPath, $meta, ['disabled' => true]); @@ -725,7 +733,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: getAttributeModel() should not be used when MAGETWO-48284 is complete $childData = $this->arrayManager->get($configPath, $meta, []); - if (($rules = $this->catalogEavValidationRules->build($this->getAttributeModel($attribute), $childData))) { + if ($rules = $this->catalogEavValidationRules->build($this->getAttributeModel($attribute), $childData)) { $meta = $this->arrayManager->merge($configPath, $meta, ['validation' => $rules]); } @@ -850,6 +858,7 @@ public function setupAttributeContainerMeta(ProductAttributeInterface $attribute 'breakLine' => false, 'label' => $attribute->getDefaultFrontendLabel(), 'required' => $attribute->getIsRequired(), + '__disableTmpl' => ['label' => true] ] ); @@ -858,7 +867,9 @@ public function setupAttributeContainerMeta(ProductAttributeInterface $attribute 'arguments/data/config', $containerMeta, [ - 'component' => 'Magento_Ui/js/form/components/group' + 'component' => 'Magento_Ui/js/form/components/group', + 'label' => false, + 'required' => false, ] ); } @@ -1048,6 +1059,10 @@ private function isScopeGlobal($attribute) */ private function getAttributeModel($attribute) { + // The statement below solves performance issue related to loading same attribute options on different models + if ($attribute instanceof EavAttribute) { + return $attribute; + } $attributeId = $attribute->getAttributeId(); if (!array_key_exists($attributeId, $this->attributesCache)) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index 91c74a2da5048..ebc0425be0188 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -21,13 +21,13 @@ class General extends AbstractModifier { /** - * @var LocatorInterface + * @var LocatorInterface * @since 101.0.0 */ protected $locator; /** - * @var ArrayManager + * @var ArrayManager * @since 101.0.0 */ protected $arrayManager; @@ -43,8 +43,8 @@ class General extends AbstractModifier private $attributeRepository; /** - * @param LocatorInterface $locator - * @param ArrayManager $arrayManager + * @param LocatorInterface $locator + * @param ArrayManager $arrayManager * @param AttributeRepositoryInterface|null $attributeRepository */ public function __construct( @@ -61,10 +61,10 @@ public function __construct( /** * Customize number fields for advanced price and weight fields. * - * @param array $data + * @param array $data * @return array * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.0.0 + * @since 101.0.0 */ public function modifyData(array $data) { @@ -72,7 +72,10 @@ public function modifyData(array $data) $data = $this->customizeAdvancedPriceFormat($data); $modelId = $this->locator->getProduct()->getId(); - if (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) { + $productStatus = $this->locator->getProduct()->getStatus(); + if (!empty($productStatus) && !empty($modelId)) { + $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = $productStatus; + } elseif (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) { $attributeStatus = $this->attributeRepository->get( ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_STATUS @@ -87,9 +90,9 @@ public function modifyData(array $data) /** * Customizing weight fields * - * @param array $data + * @param array $data * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeWeightFormat(array $data) { @@ -112,9 +115,9 @@ protected function customizeWeightFormat(array $data) /** * Customizing number fields for advanced price * - * @param array $data + * @param array $data * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeAdvancedPriceFormat(array $data) { @@ -136,9 +139,9 @@ protected function customizeAdvancedPriceFormat(array $data) /** * Customize product form fields. * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ public function modifyMeta(array $meta) { @@ -154,9 +157,9 @@ public function modifyMeta(array $meta) /** * Disable collapsible and set empty label * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function prepareFirstPanel(array $meta) { @@ -177,9 +180,9 @@ protected function prepareFirstPanel(array $meta) /** * Customize Status field * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeStatusField(array $meta) { @@ -203,9 +206,9 @@ protected function customizeStatusField(array $meta) /** * Customize Weight filed * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeWeightField(array $meta) { @@ -221,6 +224,7 @@ protected function customizeWeightField(array $meta) 'validate-zero-or-greater' => true ], 'additionalClasses' => 'admin__field-small', + 'sortOrder' => 0, 'addafter' => $this->locator->getStore()->getConfig('general/locale/weight_unit'), 'imports' => $disabled ? [] : [ 'disabled' => '!${$.provider}:' . self::DATA_SCOPE_PRODUCT @@ -239,6 +243,8 @@ protected function customizeWeightField(array $meta) $containerPath . static::META_CONFIG_PATH, $meta, [ + 'label' => false, + 'required' => false, 'component' => 'Magento_Ui/js/form/components/group', ] ); @@ -266,6 +272,7 @@ protected function customizeWeightField(array $meta) ], ], 'value' => (int)$this->locator->getProduct()->getTypeInstance()->hasWeight(), + 'sortOrder' => 10, 'disabled' => $disabled, ] ); @@ -277,9 +284,9 @@ protected function customizeWeightField(array $meta) /** * Customize "Set Product as New" date fields * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeNewDateRangeField(array $meta) { @@ -314,7 +321,8 @@ protected function customizeNewDateRangeField(array $meta) $fromContainerPath . self::META_CONFIG_PATH, $meta, [ - 'label' => __('Set Product as New From'), + 'label' => false, + 'required' => false, 'additionalClasses' => 'admin__control-grouped-date', 'breakLine' => false, 'component' => 'Magento_Ui/js/form/components/group', @@ -335,9 +343,9 @@ protected function customizeNewDateRangeField(array $meta) /** * Add links for fields depends of product name * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeNameListeners(array $meta) { @@ -409,9 +417,9 @@ private function getLocaleCurrency() /** * Format price according to the locale of the currency * - * @param mixed $value + * @param mixed $value * @return string - * @since 101.0.0 + * @since 101.0.0 */ protected function formatPrice($value) { @@ -429,9 +437,9 @@ protected function formatPrice($value) /** * Format number according to the locale of the currency and precision of input * - * @param mixed $value + * @param mixed $value * @return string - * @since 101.0.0 + * @since 101.0.0 */ protected function formatNumber($value) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php new file mode 100644 index 0000000000000..453be0c1a1582 --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; + +/** + * Additional logic on how to display the layout update field. + */ +class LayoutUpdate implements ModifierInterface +{ + /** + * @var LocatorInterface + */ + private $locator; + + /** + * @param LocatorInterface $locator + */ + public function __construct(LocatorInterface $locator) + { + $this->locator = $locator; + } + + /** + * Extract custom layout value. + * + * @param ProductInterface|Product $product + * @return mixed + */ + private function extractLayoutUpdate(ProductInterface $product) + { + if ($product instanceof Product && !$product->hasData(Product::CUSTOM_ATTRIBUTES)) { + return $product->getData('custom_layout_update'); + } + + $attr = $product->getCustomAttribute('custom_layout_update'); + + return $attr ? $attr->getValue() : null; + } + + /** + * @inheritdoc + * @since 101.1.0 + */ + public function modifyData(array $data) + { + $product = $this->locator->getProduct(); + if ($this->extractLayoutUpdate($product)) { + $data[$product->getId()][AbstractModifier::DATA_SOURCE_DEFAULT]['custom_layout_update_file'] + = \Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate::VALUE_USE_UPDATE_XML; + } + + return $data; + } + + /** + * @inheritDoc + */ + public function modifyMeta(array $meta) + { + return $meta; + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdate.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdate.php index b2f453e8d8ccb..3b01106619640 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdate.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdate.php @@ -37,7 +37,8 @@ public function __construct(ArrayManager $arrayManager) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -47,7 +48,8 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyData(array $data) @@ -96,7 +98,8 @@ protected function customizeDateRangeField(array $meta) $fromContainerPath . self::META_CONFIG_PATH, $meta, [ - 'label' => __('Schedule Update From'), + 'label' => false, + 'required' => false, 'additionalClasses' => 'admin__control-grouped-date', 'breakLine' => false, 'component' => 'Magento_Ui/js/form/components/group', diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index a529580e29239..9c5fffc5db9b9 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -115,7 +115,7 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'dataType' => Price::NAME, 'component' => 'Magento_Ui/js/form/components/group', 'label' => __('Price'), - 'enableLabel' => true, + 'showLabel' => false, 'dataScope' => '', 'additionalClasses' => 'control-grouped', 'sortOrder' => isset($priceMeta['arguments']['data']['config']['sortOrder']) diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 4fcb87ab1396e..d8f76c40e8fad 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -98,8 +98,6 @@ public function __construct( } /** - * In order to allow to use image generation using Services, we need to emulate area code and store code - * * @inheritdoc */ public function collect(ProductInterface $product, ProductRenderInterface $productRender) @@ -107,6 +105,7 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ $images = []; /** @var ThemeInterface $currentTheme */ $currentTheme = $this->design->getDesignTheme(); + $this->design->setDesignTheme($currentTheme); foreach ($this->imageCodes as $imageCode) { /** @var ImageInterface $image */ @@ -135,7 +134,6 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ $images[] = $image; } - $this->design->setDesignTheme($currentTheme); $productRender->setImages($images); } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Modifier/Attributes.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Modifier/Attributes.php new file mode 100644 index 0000000000000..01aaa6f8e0629 --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Modifier/Attributes.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product\Modifier; + +use Magento\Framework\Escaper; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; + +/** + * Modify product listing attributes + */ +class Attributes implements ModifierInterface +{ + /** + * @var Escaper + */ + private $escaper; + + /** + * @var array + */ + private $escapeAttributes; + + /** + * @param Escaper $escaper + * @param array $escapeAttributes + */ + public function __construct( + Escaper $escaper, + array $escapeAttributes = [] + ) { + $this->escaper = $escaper; + $this->escapeAttributes = $escapeAttributes; + } + + /** + * @inheritdoc + */ + public function modifyData(array $data) + { + if (!empty($data) && !empty($this->escapeAttributes)) { + foreach ($data['items'] as &$item) { + foreach ($this->escapeAttributes as $escapeAttribute) { + if (isset($item[$escapeAttribute])) { + $item[$escapeAttribute] = $this->escaper->escapeHtml($item[$escapeAttribute]); + } + } + } + } + return $data; + } + + /** + * @inheritdoc + */ + public function modifyMeta(array $meta) + { + return $meta; + } +} diff --git a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php index 27829155af292..00bac7e61b5b4 100644 --- a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php +++ b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php @@ -10,6 +10,7 @@ use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; /** * Check is available add to compare. @@ -37,7 +38,11 @@ public function __construct(StockConfigurationInterface $stockConfiguration) */ public function isAvailableForCompare(ProductInterface $product): bool { - return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock(); + if ((int)$product->getStatus() !== Status::STATUS_DISABLED) { + return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock(); + } + + return false; } /** @@ -53,6 +58,6 @@ private function isInStock(ProductInterface $product): bool return $product->isSalable(); } - return isset($quantityAndStockStatus['is_in_stock']) && $quantityAndStockStatus['is_in_stock']; + return $quantityAndStockStatus['is_in_stock'] ?? false; } } diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index fa8daaabe5710..8023634fa074d 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -31,8 +31,7 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*", - "magento/module-authorization": "*" + "magento/module-wishlist": "*" }, "suggest": { "magento/module-cookie": "*", diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index c04cfb2dce00a..17a28e5cc16eb 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -158,6 +158,10 @@ <item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\TierPrice</item> <item name="sortOrder" xsi:type="number">150</item> </item> + <item name="custom_layout_update" xsi:type="array"> + <item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\LayoutUpdate</item> + <item name="sortOrder" xsi:type="number">160</item> + </item> </argument> </arguments> </virtualType> @@ -166,7 +170,24 @@ <argument name="pool" xsi:type="object">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool</argument> </arguments> </type> - <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Listing\Modifier\Pool" type="Magento\Ui\DataProvider\Modifier\Pool"/> + <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Listing\Modifier\Pool" type="Magento\Ui\DataProvider\Modifier\Pool"> + <arguments> + <argument name="modifiers" xsi:type="array"> + <item name="attributes" xsi:type="array"> + <item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Modifier\Attributes</item> + <item name="sortOrder" xsi:type="number">10</item> + </item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Ui\DataProvider\Product\Modifier\Attributes"> + <arguments> + <argument name="escapeAttributes" xsi:type="array"> + <item name="name" xsi:type="string">name</item> + <item name="sku" xsi:type="string">sku</item> + </argument> + </arguments> + </type> <type name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions"> <arguments> <argument name="scopeName" xsi:type="string">product_form.product_form</argument> diff --git a/app/code/Magento/Catalog/etc/adminhtml/events.xml b/app/code/Magento/Catalog/etc/adminhtml/events.xml index ad83f5898237a..ab1a8348d2904 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/events.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/events.xml @@ -12,4 +12,7 @@ <event name="catalog_category_change_products"> <observer name="category_product_indexer" instance="Magento\Catalog\Observer\CategoryProductIndexer"/> </event> + <event name="category_move"> + <observer name="clean_cagegory_page_cache" instance="Magento\Catalog\Observer\FlushCategoryPagesCache" /> + </event> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index b83ed8591047a..c80363038ac60 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -65,7 +65,7 @@ </field> <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Default Value</label> - <comment>Must be in the allowed values list</comment> + <comment>Must be in the allowed values list.</comment> <validate>validate-per-page-value</validate> </field> <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -75,7 +75,7 @@ </field> <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Default Value</label> - <comment>Must be in the allowed values list</comment> + <comment>Must be in the allowed values list.</comment> <validate>validate-per-page-value</validate> </field> <field id="flat_catalog_category" translate="label" type="select" sortOrder="100" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> @@ -90,12 +90,12 @@ </field> <field id="default_sort_by" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Product Listing Sort by</label> - <comment>Applies to category pages</comment> + <comment>Applies to category pages.</comment> <source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model> </field> <field id="list_allow_all" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Allow All Products per Page</label> - <comment>Whether to show "All" option in the "Show X Per Page" dropdown</comment> + <comment>Whether to show "All" option in the "Show X Per Page" dropdown.</comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="remember_pagination" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index 3a842166a3825..20511f4ff2295 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -23,9 +23,9 @@ </fields_masks> <frontend> <list_mode>grid-list</list_mode> - <grid_per_page_values>9,15,30</grid_per_page_values> + <grid_per_page_values>12,24,36</grid_per_page_values> <list_per_page_values>5,10,15,20,25</list_per_page_values> - <grid_per_page>9</grid_per_page> + <grid_per_page>12</grid_per_page> <list_per_page>10</list_per_page> <flat_catalog_category>0</flat_catalog_category> <default_sort_by>position</default_sort_by> diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index 6fef4ca6e9128..d5b318f671726 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -812,7 +812,7 @@ <column xsi:type="smallint" name="disabled" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is Disabled"/> <column xsi:type="int" name="record_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Record Id"/> + comment="Record ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="record_id"/> </constraint> @@ -1085,7 +1085,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1114,7 +1114,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1205,7 +1205,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="smallint" name="default_store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Default store id for website"/> + comment="Default store ID for website"/> <column xsi:type="date" name="website_date" comment="Website Date"/> <column xsi:type="float" name="rate" unsigned="false" nullable="true" default="1" comment="Rate"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -1238,7 +1238,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_cfg_opt_agr_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_cfg_opt_agr_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Config Option Aggregate Temp Table"> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent ID"/> @@ -1279,7 +1279,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_cfg_opt_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_cfg_opt_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Config Option Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1327,7 +1327,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_final_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_final_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Final Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1375,7 +1375,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_opt_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_opt_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Option Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1418,7 +1418,7 @@ <column name="option_id"/> </constraint> </table> - <table name="catalog_product_index_price_opt_agr_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_opt_agr_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Option Aggregate Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1452,7 +1452,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1470,7 +1470,7 @@ <column name="value"/> </index> </table> - <table name="catalog_product_index_eav_tmp" resource="default" engine="memory" + <table name="catalog_product_index_eav_tmp" resource="default" engine="innodb" comment="Catalog Product EAV Indexer Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1481,7 +1481,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1489,13 +1489,13 @@ <column name="value"/> <column name="source_id"/> </constraint> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1510,7 +1510,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1528,7 +1528,7 @@ <column name="value"/> </index> </table> - <table name="catalog_product_index_eav_decimal_tmp" resource="default" engine="memory" + <table name="catalog_product_index_eav_decimal_tmp" resource="default" engine="innodb" comment="Catalog Product EAV Decimal Indexer Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1539,7 +1539,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1547,13 +1547,13 @@ <column name="value"/> <column name="source_id"/> </constraint> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1592,7 +1592,7 @@ <column name="min_price"/> </index> </table> - <table name="catalog_product_index_price_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1617,17 +1617,17 @@ <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="btree"> <column name="min_price"/> </index> </table> - <table name="catalog_category_product_index_tmp" resource="default" engine="memory" + <table name="catalog_category_product_index_tmp" resource="default" engine="innodb" comment="Catalog Category Product Indexer temporary table"> <column xsi:type="int" name="category_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Category ID"/> @@ -1646,7 +1646,7 @@ <column name="product_id"/> <column name="store_id"/> </constraint> - <index referenceId="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="hash"> + <index referenceId="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="btree"> <column name="product_id"/> <column name="category_id"/> <column name="store_id"/> @@ -1681,7 +1681,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1710,7 +1710,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1801,14 +1801,14 @@ <table name="catalog_product_frontend_action" resource="default" engine="innodb" comment="Catalog Product Frontend Action Table"> <column xsi:type="bigint" name="action_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Product Action Id"/> + comment="Product Action ID"/> <column xsi:type="varchar" name="type_id" nullable="false" length="64" comment="Type of product action"/> <column xsi:type="int" name="visitor_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Visitor Id"/> + comment="Visitor ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="bigint" name="added_at" padding="20" unsigned="false" nullable="false" identity="false" comment="Added At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index d4d20995a48b4..af7b9af2d8b7e 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -36,16 +36,16 @@ <preference for="Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface" type="Magento\Catalog\Model\ProductAttributeGroupRepository" /> <preference for="Magento\Catalog\Api\ProductAttributeOptionManagementInterface" type="Magento\Catalog\Model\Product\Attribute\OptionManagement" /> <preference for="Magento\Catalog\Api\ProductLinkRepositoryInterface" type="Magento\Catalog\Model\ProductLink\Repository" /> - <preference for="Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Catalog\Api\Data\ProductSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface" type="Magento\Catalog\Model\ProductAttributeSearchResults" /> + <preference for="Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface" type="Magento\Catalog\Model\CategoryAttributeSearchResults" /> + <preference for="Magento\Catalog\Api\Data\ProductSearchResultsInterface" type="Magento\Catalog\Model\ProductSearchResults" /> <preference for="Magento\Catalog\Api\ProductAttributeManagementInterface" type="Magento\Catalog\Model\Product\Attribute\Management" /> <preference for="Magento\Catalog\Api\AttributeSetManagementInterface" type="Magento\Catalog\Model\Product\Attribute\SetManagement" /> <preference for="Magento\Catalog\Api\AttributeSetRepositoryInterface" type="Magento\Catalog\Model\Product\Attribute\SetRepository" /> <preference for="Magento\Catalog\Api\ProductManagementInterface" type="Magento\Catalog\Model\ProductManagement" /> <preference for="Magento\Catalog\Api\AttributeSetFinderInterface" type="Magento\Catalog\Model\Product\Attribute\AttributeSetFinder" /> <preference for="Magento\Catalog\Api\CategoryListInterface" type="Magento\Catalog\Model\CategoryList" /> - <preference for="Magento\Catalog\Api\Data\CategorySearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Catalog\Api\Data\CategorySearchResultsInterface" type="Magento\Catalog\Model\CategorySearchResults" /> <preference for="Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface" type="Magento\Catalog\Model\Config\Source\Product\Options\Price"/> <preference for="Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterface" type="Magento\Catalog\Model\Indexer\Product\Flat\Table\Builder"/> <preference for="Magento\Catalog\Api\ProductRenderListInterface" type="Magento\Catalog\Model\ProductRenderList"/> @@ -73,6 +73,7 @@ <preference for="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactoryInterface" type="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactory" /> <preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" /> <preference for="Magento\Catalog\Api\Data\MassActionInterface" type="\Magento\Catalog\Model\MassAction" /> + <preference for="Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface" type="Magento\Catalog\Model\ProductLink\Data\ListCriteria" /> <type name="Magento\Customer\Model\ResourceModel\Visitor"> <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> @@ -402,6 +403,9 @@ <item name="upsell" xsi:type="object">Magento\Catalog\Model\ProductLink\CollectionProvider\Upsell</item> <item name="related" xsi:type="object">Magento\Catalog\Model\ProductLink\CollectionProvider\Related</item> </argument> + <argument name="mapProviders" xsi:type="array"> + <item name="linked" xsi:type="object">Magento\Catalog\Model\ProductLink\CollectionProvider\LinkedMapProvider</item> + </argument> </arguments> </type> <type name="Magento\Catalog\Model\ProductLink\Converter\ConverterPool"> @@ -411,6 +415,28 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Option"> + <arguments> + <argument name="optionGroups" xsi:type="array"> + <item name="date" xsi:type="string">Magento\Catalog\Model\Product\Option\Type\Date</item> + <item name="file" xsi:type="string">Magento\Catalog\Model\Product\Option\Type\File</item> + <item name="select" xsi:type="string">Magento\Catalog\Model\Product\Option\Type\Select</item> + <item name="text" xsi:type="string">Magento\Catalog\Model\Product\Option\Type\Text</item> + </argument> + <argument name="optionTypesToGroups" xsi:type="array"> + <item name="field" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_TEXT</item> + <item name="area" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_TEXT</item> + <item name="file" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_FILE</item> + <item name="drop_down" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT</item> + <item name="radio" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT</item> + <item name="checkbox" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT</item> + <item name="multiple" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT</item> + <item name="date" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_DATE</item> + <item name="date_time" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_DATE</item> + <item name="time" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_DATE</item> + </argument> + </arguments> + </type> <type name="Magento\Catalog\Model\Product\Option\Validator\Pool"> <arguments> <argument name="validators" xsi:type="array"> @@ -867,7 +893,7 @@ <argument name="hydrators" xsi:type="array"> <item name="Magento\Catalog\Api\Data\CategoryInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item> <item name="Magento\Catalog\Api\Data\CategoryTreeInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item> - <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item> + <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="string">Magento\Catalog\Model\Product\Hydrator</item> </argument> </arguments> </type> @@ -1176,4 +1202,124 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\ProductLink\Repository"> + <arguments> + <argument name="entityCollectionProvider" xsi:type="object">Magento\Catalog\Model\ProductLink\CollectionProvider\Proxy</argument> + <argument name="linkInitializer" xsi:type="object">Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ProductLink\ProductLinkQuery"> + <arguments> + <argument name="collectionProvider" xsi:type="object">Magento\Catalog\Model\ProductLink\CollectionProvider\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager"> + <arguments> + <argument name="themeFactory" xsi:type="object">Magento\Framework\View\Design\Theme\FlyweightFactory\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager"> + <arguments> + <argument name="themeFactory" xsi:type="object">Magento\Framework\View\Design\Theme\FlyweightFactory\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Category\Attribute\Source\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Attribute\Source\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="category_ids" xsi:type="string">catalog_product</item> + <item name="country_of_manufacture" xsi:type="string">catalog_product</item> + <item name="created_at" xsi:type="string">catalog_product</item> + <item name="custom_design" xsi:type="string">catalog_product</item> + <item name="custom_design_from" xsi:type="string">catalog_product</item> + <item name="custom_design_to" xsi:type="string">catalog_product</item> + <item name="custom_layout" xsi:type="string">catalog_product</item> + <item name="custom_layout_update" xsi:type="string">catalog_product</item> + <item name="description" xsi:type="string">catalog_product</item> + <item name="gallery" xsi:type="string">catalog_product</item> + <item name="has_options" xsi:type="string">catalog_product</item> + <item name="image" xsi:type="string">catalog_product</item> + <item name="image_label" xsi:type="string">catalog_product</item> + <item name="media_gallery" xsi:type="string">catalog_product</item> + <item name="meta_description" xsi:type="string">catalog_product</item> + <item name="meta_keyword" xsi:type="string">catalog_product</item> + <item name="meta_title" xsi:type="string">catalog_product</item> + <item name="minimal_price" xsi:type="string">catalog_product</item> + <item name="name" xsi:type="string">catalog_product</item> + <item name="news_from_date" xsi:type="string">catalog_product</item> + <item name="news_to_date" xsi:type="string">catalog_product</item> + <item name="old_id" xsi:type="string">catalog_product</item> + <item name="options_container" xsi:type="string">catalog_product</item> + <item name="page_layout" xsi:type="string">catalog_product</item> + <item name="price" xsi:type="string">catalog_product</item> + <item name="quantity_and_stock_status" xsi:type="string">catalog_product</item> + <item name="required_options" xsi:type="string">catalog_product</item> + <item name="short_description" xsi:type="string">catalog_product</item> + <item name="sku" xsi:type="string">catalog_product</item> + <item name="small_image" xsi:type="string">catalog_product</item> + <item name="small_image_label" xsi:type="string">catalog_product</item> + <item name="special_from_date" xsi:type="string">catalog_product</item> + <item name="special_price" xsi:type="string">catalog_product</item> + <item name="special_to_date" xsi:type="string">catalog_product</item> + <item name="status" xsi:type="string">catalog_product</item> + <item name="thumbnail" xsi:type="string">catalog_product</item> + <item name="thumbnail_label" xsi:type="string">catalog_product</item> + <item name="tier_price" xsi:type="string">catalog_product</item> + <item name="updated_at" xsi:type="string">catalog_product</item> + <item name="visibility" xsi:type="string">catalog_product</item> + <item name="weight" xsi:type="string">catalog_product</item> + </item> + <item name="catalog_category" xsi:type="array"> + <item name="all_children" xsi:type="string">catalog_category</item> + <item name="available_sort_by" xsi:type="string">catalog_category</item> + <item name="children" xsi:type="string">catalog_category</item> + <item name="children_count" xsi:type="string">catalog_category</item> + <item name="custom_apply_to_products" xsi:type="string">catalog_category</item> + <item name="custom_design" xsi:type="string">catalog_category</item> + <item name="custom_design_from" xsi:type="string">catalog_category</item> + <item name="custom_design_to" xsi:type="string">catalog_category</item> + <item name="custom_layout_update" xsi:type="string">catalog_category</item> + <item name="custom_use_parent_settings" xsi:type="string">catalog_category</item> + <item name="default_sort_by" xsi:type="string">catalog_category</item> + <item name="description" xsi:type="string">catalog_category</item> + <item name="display_mode" xsi:type="string">catalog_category</item> + <item name="filter_price_range" xsi:type="string">catalog_category</item> + <item name="image" xsi:type="string">catalog_category</item> + <item name="include_in_menu" xsi:type="string">catalog_category</item> + <item name="is_active" xsi:type="string">catalog_category</item> + <item name="is_anchor" xsi:type="string">catalog_category</item> + <item name="landing_page" xsi:type="string">catalog_category</item> + <item name="level" xsi:type="string">catalog_category</item> + <item name="meta_description" xsi:type="string">catalog_category</item> + <item name="meta_keywords" xsi:type="string">catalog_category</item> + <item name="meta_title" xsi:type="string">catalog_category</item> + <item name="name" xsi:type="string">catalog_category</item> + <item name="page_layout" xsi:type="string">catalog_category</item> + <item name="path" xsi:type="string">catalog_category</item> + <item name="path_in_store" xsi:type="string">catalog_category</item> + <item name="position" xsi:type="string">catalog_category</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml index f4345ce719a19..24186146c56f0 100644 --- a/app/code/Magento/Catalog/etc/events.xml +++ b/app/code/Magento/Catalog/etc/events.xml @@ -64,4 +64,7 @@ <event name="catalog_product_save_commit_after"> <observer name="magento_image_resize" instance="Magento\Catalog\Observer\ImageResizeAfterProductSave" /> </event> + <event name="catalog_category_prepare_save"> + <observer name="additional_authorization" instance="Magento\Catalog\Observer\CategoryDesignAuthorization" /> + </event> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 44cdd473bf74e..bfbc05b12079d 100644 --- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml @@ -22,4 +22,10 @@ <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> </type> + <type name="Magento\Catalog\Api\ProductRepositoryInterface"> + <plugin name="product_authorization" type="Magento\Catalog\Plugin\ProductAuthorization" /> + </type> + <type name="Magento\Catalog\Api\CategoryRepositoryInterface"> + <plugin name="category_authorization" type="Magento\Catalog\Plugin\CategoryAuthorization" /> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml index 44cdd473bf74e..bfbc05b12079d 100644 --- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml @@ -22,4 +22,10 @@ <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> </type> + <type name="Magento\Catalog\Api\ProductRepositoryInterface"> + <plugin name="product_authorization" type="Magento\Catalog\Plugin\ProductAuthorization" /> + </type> + <type name="Magento\Catalog\Api\CategoryRepositoryInterface"> + <plugin name="category_authorization" type="Magento\Catalog\Plugin\CategoryAuthorization" /> + </type> </config> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index 9b7f8a2b07730..555871ef32c26 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -814,4 +814,5 @@ Details,Details "A total of %1 record(s) haven't been deleted. Please see server logs for more details.","A total of %1 record(s) haven't been deleted. Please see server logs for more details." "Are you sure you want to delete this category?","Are you sure you want to delete this category?" "Attribute Set Information","Attribute Set Information" +"Failed to retrieve product links for ""%1""","Failed to retrieve product links for ""%1""" diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index 6b63a20134df1..d6340330df8ea 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -507,8 +507,13 @@ })(jQuery); this.closeModal(); } - }] - + }], + keyEventHandlers: { + enterKey: function (event) { + this.buttons[1].click(); + event.preventDefault(); + } + } }).trigger('openModal'); } diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml index 8adffb752187b..68a7a3a69cfd3 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml @@ -7,7 +7,7 @@ <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> <?php $_option = $block->getOption(); ?> <?php $_optionId = (int)$_option->getId(); ?> -<div class="admin__field field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> +<div class="admin__field field<?= $_option->getIsRequire() ? ' required' : '' ?>"> <label class="label admin__field-label"> <?= $block->escapeHtml($_option->getTitle()) ?> <?= /* @noEscape */ $block->getFormattedPrice() ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml index 2218ce5d29671..243fdb6e603a0 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml @@ -6,7 +6,7 @@ ?> <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select */ ?> <?php $_option = $block->getOption(); ?> -<div class="admin__field field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> +<div class="admin__field field<?= $_option->getIsRequire() ? ' _required' : '' ?>"> <label class="label admin__field-label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index 992093c4a6658..f2afef1215017 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -489,6 +489,15 @@ </imports> </settings> </field> + <field name="custom_layout_update_file" component="Magento_Catalog/js/components/use-parent-settings/select" sortOrder="205" formElement="select"> + <settings> + <dataType>string</dataType> + <label translate="true">Custom Layout Update</label> + <imports> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> + </imports> + </settings> + </field> <field name="custom_apply_to_products" component="Magento_Catalog/js/components/use-parent-settings/single-checkbox" sortOrder="210" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -528,10 +537,7 @@ <item name="type" xsi:type="string">group</item> <item name="config" xsi:type="array"> <item name="additionalClasses" xsi:type="string">admin__control-grouped-date</item> - <item name="label" xsi:type="string" translate="true">Schedule Update From</item> - <item name="required" xsi:type="boolean">false</item> <item name="breakLine" xsi:type="boolean">false</item> - <item name="scopeLabel" xsi:type="string">[STORE VIEW]</item> </item> </argument> <field name="custom_design_from" sortOrder="230" formElement="date"> @@ -541,6 +547,7 @@ </additionalClasses> <dataType>string</dataType> <label translate="true">Schedule Update From</label> + <scopeLabel>[STORE VIEW]</scopeLabel> </settings> </field> <field name="custom_design_to" sortOrder="240" formElement="date"> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/new-attribute-form.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/new-attribute-form.js index 6702b94b119de..a879b85bab257 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/new-attribute-form.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/new-attribute-form.js @@ -42,41 +42,47 @@ define([ var self = this; - prompt({ - content: this.newSetPromptMessage, - actions: { + this.validate(); - /** - * @param {String} val - * @this {actions} - */ - confirm: function (val) { - var rules = ['required-entry', 'validate-no-html-tags'], - editForm = self, - newAttributeSetName = val, - i, - params = {}; + if (!this.additionalInvalid && !this.source.get('params.invalid')) { + prompt({ + content: this.newSetPromptMessage, + actions: { - if (!newAttributeSetName) { - return; - } - - for (i = 0; i < rules.length; i++) { - if (!$.validator.methods[rules[i]](newAttributeSetName)) { - alert({ - content: $.validator.messages[rules[i]] - }); + /** + * @param {String} val + * @this {actions} + */ + confirm: function (val) { + var rules = ['required-entry', 'validate-no-html-tags'], + editForm = self, + newAttributeSetName = val, + i, + params = {}; + if (!newAttributeSetName) { return; } - } - params['new_attribute_set_name'] = newAttributeSetName; - editForm.setAdditionalData(params); - editForm.save(); + for (i = 0; i < rules.length; i++) { + if (!$.validator.methods[rules[i]](newAttributeSetName)) { + alert({ + content: $.validator.messages[rules[i]] + }); + + return; + } + } + + params['new_attribute_set_name'] = newAttributeSetName; + editForm.setAdditionalData(params); + editForm.save(); + } } - } - }); + }); + } else { + this.focusInvalid(); + } } }); }); diff --git a/app/code/Magento/Catalog/view/base/requirejs-config.js b/app/code/Magento/Catalog/view/base/requirejs-config.js new file mode 100644 index 0000000000000..2fc0bbcb8c00d --- /dev/null +++ b/app/code/Magento/Catalog/view/base/requirejs-config.js @@ -0,0 +1,16 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + priceBox: 'Magento_Catalog/js/price-box', + priceOptionDate: 'Magento_Catalog/js/price-option-date', + priceOptionFile: 'Magento_Catalog/js/price-option-file', + priceOptions: 'Magento_Catalog/js/price-options', + priceUtils: 'Magento_Catalog/js/price-utils' + } + } +}; diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml index dbc064665d3fe..78d2883d7401a 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -53,7 +53,7 @@ if ($option) : ?> } ?> - <div class="field choice admin__field admin__field-option <?= /* @noEscape */ $option->getIsRequire() ? 'required': '' ?>"> + <div class="field choice admin__field admin__field-option"> <input type="<?= $block->escapeHtmlAttr($optionType) ?>" class="<?= $optionType === Option::OPTION_TYPE_RADIO ? 'radio admin__control-radio' diff --git a/app/code/Magento/Catalog/view/frontend/requirejs-config.js b/app/code/Magento/Catalog/view/frontend/requirejs-config.js index 55df18afeb024..3674d54340544 100644 --- a/app/code/Magento/Catalog/view/frontend/requirejs-config.js +++ b/app/code/Magento/Catalog/view/frontend/requirejs-config.js @@ -11,11 +11,6 @@ var config = { upsellProducts: 'Magento_Catalog/js/upsell-products', productListToolbarForm: 'Magento_Catalog/js/product/list/toolbar', catalogGallery: 'Magento_Catalog/js/gallery', - priceBox: 'Magento_Catalog/js/price-box', - priceOptionDate: 'Magento_Catalog/js/price-option-date', - priceOptionFile: 'Magento_Catalog/js/price-option-file', - priceOptions: 'Magento_Catalog/js/price-options', - priceUtils: 'Magento_Catalog/js/price-utils', catalogAddToCart: 'Magento_Catalog/js/catalog-add-to-cart' } }, diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 55772388d44bf..0bea3ca03dee8 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -130,6 +130,8 @@ default :?> <?php if (is_string($block->getProductAttributeValue($item, $attribute))) :?> <?= /* @noEscape */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> + <?php else : ?> + <?= $block->escapeHtml($helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode())) ?> <?php endif; ?> <?php break; } ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index ce44884a575b8..554caf6026001 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -13,14 +13,16 @@ use Magento\Framework\App\Action\Action; * Product list template * * @var $block \Magento\Catalog\Block\Product\ListProduct + * @var \Magento\Framework\Escaper $escaper */ ?> <?php $_productCollection = $block->getLoadedProductCollection(); +/** @var \Magento\Catalog\Helper\Output $_helper */ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> <?php if (!$_productCollection->count()) :?> - <div class="message info empty"><div><?= $block->escapeHtml(__('We can\'t find products matching the selection.')) ?></div></div> + <div class="message info empty"><div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div></div> <?php else :?> <?= $block->getToolbarHtml() ?> <?= $block->getAdditionalHtml() ?> @@ -55,7 +57,7 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); } ?> <?php // Product Image ?> - <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>" class="product photo product-item-photo" tabindex="-1"> <?= $productImage->toHtml() ?> @@ -66,22 +68,24 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> <strong class="product name product-item-name"> <a class="product-item-link" - href="<?= $block->escapeUrl($_product->getProductUrl()) ?>"> + href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"> <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> </a> </strong> <?= $block->getReviewsSummaryHtml($_product, $templateType) ?> <?= /* @noEscape */ $block->getProductPrice($_product) ?> - <?= $block->getProductDetailsHtml($_product) ?> + <?php if ($_product->isAvailable()) :?> + <?= $block->getProductDetailsHtml($_product) ?> + <?php endif; ?> <div class="product-item-inner"> - <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $block->escapeHtmlAttr($position) : '' ?>> - <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $block->escapeHtmlAttr($position) : '' ?>> + <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $escaper->escapeHtmlAttr($position) : '' ?>> + <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $escaper->escapeHtmlAttr($position) : '' ?>> <?php if ($_product->isSaleable()) :?> <?php $postParams = $block->getAddToCartPostParams($_product); ?> <form data-role="tocart-form" - data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>" - action="<?= $block->escapeUrl($postParams['action']) ?>" + data-product-sku="<?= $escaper->escapeHtml($_product->getSku()) ?>" + action="<?= $escaper->escapeUrl($postParams['action']) ?>" method="post"> <input type="hidden" name="product" @@ -90,20 +94,20 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> <?= $block->getBlockHtml('formkey') ?> <button type="submit" - title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" + title="<?= $escaper->escapeHtmlAttr(__('Add to Cart')) ?>" class="action tocart primary"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + <span><?= $escaper->escapeHtml(__('Add to Cart')) ?></span> </button> </form> <?php else :?> <?php if ($_product->isAvailable()) :?> - <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <div class="stock available"><span><?= $escaper->escapeHtml(__('In stock')) ?></span></div> <?php else :?> - <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <div class="stock unavailable"><span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> - <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $block->escapeHtmlAttr($position) : '' ?>> + <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $escaper->escapeHtmlAttr($position) : '' ?>> <?php if ($addToBlock = $block->getChildBlock('addto')) :?> <?= $addToBlock->setProduct($_product)->getChildHtml() ?> <?php endif; ?> @@ -112,9 +116,9 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <?php if ($showDescription) :?> <div class="product description product-item-description"> <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> - <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>" title="<?= /* @noEscape */ $_productNameStripped ?>" - class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> + class="action more"><?= $escaper->escapeHtml(__('Learn More')) ?></a> </div> <?php endif; ?> </div> @@ -130,7 +134,7 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); { "[data-role=tocart-form], .form.map.checkout": { "catalogAddToCart": { - "product_sku": "<?= $block->escapeJs($_product->getSku()) ?>" + "product_sku": "<?= $escaper->escapeJs($_product->getSku()) ?>" } } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml index b776fd4f7e193..6cebd51284f48 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml @@ -16,7 +16,6 @@ */ ?> <?php -$start = microtime(true); $_productCollection = $block->getLoadedProductCollection(); $_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> @@ -98,4 +97,3 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); </div> <?= $block->getToolbarHtml() ?> <?php endif; ?> -<?= $time_taken = microtime(true) - $start ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml index c6d351b2a9571..25257f4bcea8a 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml @@ -22,7 +22,7 @@ <input type="hidden" name="product" value="<?= (int)$_product->getId() ?>" /> <input type="hidden" name="selected_configurable_option" value="" /> <input type="hidden" name="related_product" id="related-products-field" value="" /> - <input type="hidden" name="item" value="<?= $block->escapeHtmlAttr($block->getRequest()->getParam('id')) ?>" /> + <input type="hidden" name="item" value="<?= (int)$block->getRequest()->getParam('id') ?>" /> <?= $block->getBlockHtml('formkey') ?> <?= $block->getChildHtml('form_top') ?> <?php if (!$block->hasOptions()) :?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml index eb2bde647f9b1..4bfdbb7bc24bc 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml @@ -9,13 +9,16 @@ <meta property="og:type" content="product" /> <meta property="og:title" - content="<?= /* @noEscape */ $block->stripTags($block->getProduct()->getName()) ?>" /> + content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getName())) ?>" /> <meta property="og:image" content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> <meta property="og:description" - content="<?= /* @noEscape */ $block->stripTags($block->getProduct()->getShortDescription()) ?>" /> + content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getShortDescription())) ?>" /> <meta property="og:url" content="<?= $block->escapeUrl($block->getProduct()->getProductUrl()) ?>" /> -<?php if ($priceAmount = $block->getProduct()->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)->getAmount()) :?> +<?php if ($priceAmount = $block->getProduct() + ->getPriceInfo() + ->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) + ->getAmount()):?> <meta property="product:price:amount" content="<?= $block->escapeHtmlAttr($priceAmount) ?>"/> <?= $block->getChildHtml('meta.currency') ?> <?php endif;?> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js index f6be6fd58ca25..2b3349c25c917 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js @@ -3,18 +3,10 @@ * See COPYING.txt for license details. */ -(function (factory) { - 'use strict'; - - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'jquery-ui-modules/widget' - ], factory); - } else { - factory(jQuery); - } -}(function ($) { +define([ + 'jquery', + 'jquery-ui-modules/widget' +], function ($) { 'use strict'; $.widget('mage.gallery', { @@ -49,4 +41,4 @@ }); return $.mage.gallery; -})); +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js index 822dd5b9a7b13..c875dd8f5d2c7 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js @@ -26,8 +26,8 @@ define([ * @private */ _create: function () { - $(this.options.selectAllLink).on('click', $.proxy(this._selectAllRelated, this)); - $(this.options.relatedCheckbox).on('click', $.proxy(this._addRelatedToProduct, this)); + $(this.options.selectAllLink, this.element).on('click', $.proxy(this._selectAllRelated, this)); + $(this.options.relatedCheckbox, this.element).on('click', $.proxy(this._addRelatedToProduct, this)); this._showRelatedProducts( this.element.find(this.options.elementsSelector), this.element.data('limit'), diff --git a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js index ab1753e7b9ed3..3205e58297b6c 100644 --- a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js +++ b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js @@ -3,19 +3,11 @@ * See COPYING.txt for license details. */ -(function (factory) { - 'use strict'; - - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'jquery-ui-modules/widget', - 'mage/validation/validation' - ], factory); - } else { - factory(jQuery); - } -}(function ($) { +define([ + 'jquery', + 'jquery-ui-modules/widget', + 'mage/validation/validation' +], function ($) { 'use strict'; $.widget('mage.validation', $.mage.validation, { @@ -97,4 +89,4 @@ }); return $.mage.validation; -})); +}); diff --git a/app/code/Magento/CatalogCmsGraphQl/Model/Resolver/Category/Block.php b/app/code/Magento/CatalogCmsGraphQl/Model/Resolver/Category/Block.php new file mode 100644 index 0000000000000..110d03cac7735 --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/Model/Resolver/Category/Block.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCmsGraphQl\Model\Resolver\Category; + +use Magento\Catalog\Model\Category; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CmsGraphQl\Model\Resolver\DataProvider\Block as BlockProvider; + +/** + * Resolver category cms content + */ +class Block implements ResolverInterface +{ + /** + * @var BlockProvider + */ + private $blockProvider; + + /** + * @param BlockProvider $blockProvider + */ + public function __construct(BlockProvider $blockProvider) + { + $this->blockProvider = $blockProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Category $category */ + $category = $value['model']; + $blockId = $category->getLandingPage(); + + if (empty($blockId)) { + return null; + } + + try { + $block = $this->blockProvider->getData($blockId); + } catch (NoSuchEntityException $e) { + return null; + } + + return $block; + } +} diff --git a/app/code/Magento/CatalogCmsGraphQl/README.md b/app/code/Magento/CatalogCmsGraphQl/README.md new file mode 100644 index 0000000000000..f3b36e515ac6e --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/README.md @@ -0,0 +1,3 @@ +# CatalogCmsGraphQl + +**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules. \ No newline at end of file diff --git a/app/code/Magento/CatalogCmsGraphQl/composer.json b/app/code/Magento/CatalogCmsGraphQl/composer.json new file mode 100644 index 0000000000000..a9d6ee4d9f2f1 --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-catalog-cms-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-cms-graph-ql": "*" + }, + "suggest": { + "magento/module-graph-ql": "*", + "magento/module-cms": "*", + "magento/module-catalog-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogCmsGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CatalogCmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogCmsGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..cc8d8f9845c51 --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/etc/graphql/di.xml @@ -0,0 +1,18 @@ +<?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\CatalogGraphQl\Model\AttributesJoiner"> + <arguments> + <argument name="fieldToAttributeMap" xsi:type="array"> + <item name="cms_block" xsi:type="array"> + <item name="landing_page" xsi:type="string">landing_page</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CatalogCmsGraphQl/etc/module.xml b/app/code/Magento/CatalogCmsGraphQl/etc/module.xml new file mode 100644 index 0000000000000..40cc556bbf713 --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/etc/module.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:Module/etc/module.xsd"> + <module name="Magento_CatalogCmsGraphQl" > + <sequence> + <module name="Magento_CmsGraphQl"/> + <module name="Magento_CatalogGraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CatalogCmsGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogCmsGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..0fc5f69a009a4 --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/etc/schema.graphqls @@ -0,0 +1,6 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface CategoryInterface { + cms_block: CmsBlock @doc(description: "Category CMS Block.") @resolver(class: "Magento\\CatalogCmsGraphQl\\Model\\Resolver\\Category\\Block") +} \ No newline at end of file diff --git a/app/code/Magento/CatalogCmsGraphQl/registration.php b/app/code/Magento/CatalogCmsGraphQl/registration.php new file mode 100644 index 0000000000000..c2b95cd9a4c5d --- /dev/null +++ b/app/code/Magento/CatalogCmsGraphQl/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_CatalogCmsGraphQl', __DIR__); diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Customer/GetCustomerGroup.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Customer/GetCustomerGroup.php new file mode 100644 index 0000000000000..65ab2940a7a51 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Customer/GetCustomerGroup.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver\Customer; + +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\GroupManagement; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; + +/** + * Get customer group + */ +class GetCustomerGroup +{ + /** + * @var GroupManagementInterface + */ + private $groupManagement; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @param GroupManagementInterface $groupManagement + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + GroupManagementInterface $groupManagement, + CustomerRepositoryInterface $customerRepository + ) { + $this->groupManagement = $groupManagement; + $this->customerRepository = $customerRepository; + } + + /** + * Get customer group by id + * + * @param int|null $customerId + * @return int + * @throws GraphQlNoSuchEntityException + */ + public function execute(?int $customerId): int + { + if (!$customerId) { + $customerGroupId = GroupManagement::NOT_LOGGED_IN_ID; + } else { + try { + $customer = $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]), + $e + ); + } + $customerGroupId = $customer->getGroupId(); + } + return (int)$customerGroupId; + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php new file mode 100644 index 0000000000000..4e75139c1a882 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\Tiers; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\TiersFactory; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; +use Magento\Store\Api\Data\StoreInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; +use Magento\Catalog\Api\Data\ProductTierPriceInterface; + +/** + * Resolver for price_tiers + */ +class PriceTiers implements ResolverInterface +{ + /** + * @var TiersFactory + */ + private $tiersFactory; + + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var GetCustomerGroup + */ + private $getCustomerGroup; + + /** + * @var int + */ + private $customerGroupId; + + /** + * @var Tiers + */ + private $tiers; + + /** + * @var Discount + */ + private $discount; + + /** + * @var PriceProviderPool + */ + private $priceProviderPool; + + /** + * @param ValueFactory $valueFactory + * @param TiersFactory $tiersFactory + * @param GetCustomerGroup $getCustomerGroup + * @param Discount $discount + * @param PriceProviderPool $priceProviderPool + */ + public function __construct( + ValueFactory $valueFactory, + TiersFactory $tiersFactory, + GetCustomerGroup $getCustomerGroup, + Discount $discount, + PriceProviderPool $priceProviderPool + ) { + $this->valueFactory = $valueFactory; + $this->tiersFactory = $tiersFactory; + $this->getCustomerGroup = $getCustomerGroup; + $this->discount = $discount; + $this->priceProviderPool = $priceProviderPool; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (empty($this->tiers)) { + $this->customerGroupId = $this->getCustomerGroup->execute($context->getUserId()); + $this->tiers = $this->tiersFactory->create(['customerGroupId' => $this->customerGroupId]); + } + + $product = $value['model']; + $productId = $product->getId(); + $this->tiers->addProductFilter($productId); + + return $this->valueFactory->create( + function () use ($productId, $context) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + $productPrice = $this->tiers->getProductRegularPrice($productId) ?? 0.0; + $tierPrices = $this->tiers->getProductTierPrices($productId) ?? []; + + return $this->formatProductTierPrices($tierPrices, $productPrice, $store); + } + ); + } + + /** + * Format tier prices for output + * + * @param ProductTierPriceInterface[] $tierPrices + * @param float $productPrice + * @param StoreInterface $store + * @return array + */ + private function formatProductTierPrices(array $tierPrices, float $productPrice, StoreInterface $store): array + { + $tiers = []; + + foreach ($tierPrices as $tierPrice) { + $percentValue = $tierPrice->getExtensionAttributes()->getPercentageValue(); + if ($percentValue && is_numeric($percentValue)) { + $discount = $this->discount->getDiscountByPercent($productPrice, (float)$percentValue); + } else { + $discount = $this->discount->getDiscountByDifference($productPrice, (float)$tierPrice->getValue()); + } + + $tiers[] = [ + "discount" => $discount, + "quantity" => $tierPrice->getQty(), + "final_price" => [ + "value" => $tierPrice->getValue(), + "currency" => $store->getCurrentCurrencyCode() + ] + ]; + } + return $tiers; + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php new file mode 100644 index 0000000000000..73a2ba83d5096 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Customer\Model\GroupManagement; +use Magento\Catalog\Api\Data\ProductTierPriceInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; + +/** + * Get product tier price information + */ +class Tiers +{ + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var ProductResource + */ + private $productResource; + + /** + * @var PriceProviderPool + */ + private $priceProviderPool; + + /** + * @var bool + */ + private $loaded = false; + + /** + * @var int + */ + private $customerGroupId = GroupManagement::CUST_GROUP_ALL; + + /** + * @var array + */ + private $filterProductIds = []; + + /** + * @var array + */ + private $products = []; + + /** + * @param CollectionFactory $collectionFactory + * @param ProductResource $productResource + * @param PriceProviderPool $priceProviderPool + * @param int $customerGroupId + */ + public function __construct( + CollectionFactory $collectionFactory, + ProductResource $productResource, + PriceProviderPool $priceProviderPool, + $customerGroupId + ) { + $this->collectionFactory = $collectionFactory; + $this->productResource = $productResource; + $this->priceProviderPool = $priceProviderPool; + $this->customerGroupId = $customerGroupId; + } + + /** + * Add product ID to collection filter + * + * @param int $productId + */ + public function addProductFilter($productId): void + { + $this->filterProductIds[] = $productId; + } + + /** + * Get tier prices for product by ID + * + * @param int $productId + * @return ProductTierPriceInterface[]|null + */ + public function getProductTierPrices($productId): ?array + { + if (!$this->isLoaded()) { + $this->load(); + } + + if (empty($this->products[$productId])) { + return null; + } + return $this->products[$productId]->getTierPrices(); + } + + /** + * Get product regular price by ID + * + * @param int $productId + * @return float|null + */ + public function getProductRegularPrice($productId): ?float + { + if (!$this->isLoaded()) { + $this->load(); + } + + if (empty($this->products[$productId])) { + return null; + } + $product = $this->products[$productId]; + $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId()); + return $priceProvider->getRegularPrice($product)->getValue(); + } + + /** + * Check if collection has been loaded + * + * @return bool + */ + public function isLoaded(): bool + { + $numFilterProductIds = count(array_unique($this->filterProductIds)); + if ($numFilterProductIds > count($this->products)) { + //New products were added to the filter after load, so we should reload + return false; + } + return $this->loaded; + } + + /** + * Load product collection + */ + private function load(): void + { + $this->loaded = false; + + $productIdField = $this->productResource->getEntityIdField(); + /** @var Collection $productCollection */ + $productCollection = $this->collectionFactory->create(); + $productCollection->addFieldToFilter($productIdField, ['in' => $this->filterProductIds]); + $productCollection->addAttributeToSelect('price'); + $productCollection->addAttributeToSelect('price_type'); + $productCollection->load(); + $productCollection->addTierPriceDataByGroupId($this->customerGroupId); + + $this->setProducts($productCollection); + $this->loaded = true; + } + + /** + * Set products from collection + * + * @param Collection $productCollection + */ + private function setProducts(Collection $productCollection): void + { + $this->products = []; + + foreach ($productCollection as $product) { + $this->products[$product->getId()] = $product; + } + + $missingProducts = array_diff($this->filterProductIds, array_keys($this->products)); + foreach (array_unique($missingProducts) as $missingProductId) { + $this->products[$missingProductId] = null; + } + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php new file mode 100644 index 0000000000000..c449d0a2ba30b --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver; + +use Magento\Catalog\Model\Product; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\Tiers; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\TiersFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; + +/** + * @inheritdoc + */ +class TierPrices implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var int + */ + private $customerGroupId = null; + + /** + * @var Tiers + */ + private $tiers; + + /** + * @var TiersFactory + */ + private $tiersFactory; + + /** + * @var GetCustomerGroup + */ + private $getCustomerGroup; + + /** + * @param ValueFactory $valueFactory + * @param TiersFactory $tiersFactory + * @param GetCustomerGroup $getCustomerGroup + */ + public function __construct( + ValueFactory $valueFactory, + TiersFactory $tiersFactory, + GetCustomerGroup $getCustomerGroup + ) { + $this->valueFactory = $valueFactory; + $this->tiersFactory = $tiersFactory; + $this->getCustomerGroup = $getCustomerGroup; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (null === $this->customerGroupId) { + $this->customerGroupId = $this->getCustomerGroup->execute($context->getUserId()); + $this->tiers = $this->tiersFactory->create(['customerGroupId' => $this->customerGroupId]); + } + + /** @var Product $product */ + $product = $value['model']; + $productId = $product->getId(); + $this->tiers->addProductFilter($productId); + + return $this->valueFactory->create( + function () use ($productId, $context) { + $tierPrices = $this->tiers->getProductTierPrices($productId); + + return $tierPrices ?? []; + } + ); + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/README.md b/app/code/Magento/CatalogCustomerGraphQl/README.md new file mode 100644 index 0000000000000..525a1a4f76433 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/README.md @@ -0,0 +1,3 @@ +# CatalogCustomerGraphQl + +**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules. \ No newline at end of file diff --git a/app/code/Magento/CatalogCustomerGraphQl/composer.json b/app/code/Magento/CatalogCustomerGraphQl/composer.json new file mode 100644 index 0000000000000..859a5c6235697 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-catalog-customer-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-store": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogCustomerGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml b/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml new file mode 100644 index 0000000000000..6131435258b58 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/etc/module.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:Module/etc/module.xsd"> + <module name="Magento_CatalogCustomerGraphQl" > + <sequence> + <module name="Magento_Catalog"/> + <module name="Magento_Customer"/> + <module name="Magento_CatalogGraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..17880584bf160 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls @@ -0,0 +1,22 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface ProductInterface { + tier_prices: [ProductTierPrices] @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\TierPrices") + price_tiers: [TierPrice] @doc(description: "An array of TierPrice objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\PriceTiers") +} + +type ProductTierPrices @doc(description: "ProductTierPrices is deprecated and has been replaced by TierPrice. The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") { + customer_group_id: String @deprecated(reason: "customer_group_id is not relevant for storefront.") @doc(description: "The ID of the customer group.") + qty: Float @deprecated(reason: "ProductTierPrices is deprecated, use TierPrice.quantity.") @doc(description: "The number of items that must be purchased to qualify for tier pricing.") + value: Float @deprecated(reason: "ProductTierPrices is deprecated. Use TierPrice.final_price") @doc(description: "The price of the fixed price item.") + percentage_value: Float @deprecated(reason: "ProductTierPrices is deprecated. Use TierPrice.discount.") @doc(description: "The percentage discount of the item.") + website_id: Float @deprecated(reason: "website_id is not relevant for storefront.") @doc(description: "The ID assigned to the website.") +} + + +type TierPrice @doc(description: "A price based on the quantity purchased.") { + final_price: Money @doc(desription: "The price of the product at this tier.") + quantity: Float @doc(description: "The minimum number of items that must be purchased to qualify for this price tier.") + discount: ProductDiscount @doc(description: "The price discount that this tier represents.") +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/registration.php b/app/code/Magento/CatalogCustomerGraphQl/registration.php new file mode 100644 index 0000000000000..8176716d42ea0 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/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_CatalogCustomerGraphQl', __DIR__); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php new file mode 100644 index 0000000000000..b0f085932bb8e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php @@ -0,0 +1,354 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider; + +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Generic for build Select object to fetch eav attributes for provided entity type + */ +class AttributeQuery +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var string + */ + private $entityType; + + /** + * List of attributes that need to be added/removed to fetch + * + * @var array + */ + private $linkedAttributes; + + /** + * @var array + */ + private const SUPPORTED_BACKEND_TYPES = [ + 'int', + 'decimal', + 'text', + 'varchar', + 'datetime', + ]; + + /** + * @var int[] + */ + private $entityTypeIdMap; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @param string $entityType + * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool + * @param Config $eavConfig + * @param array $linkedAttributes + */ + public function __construct( + string $entityType, + ResourceConnection $resourceConnection, + MetadataPool $metadataPool, + Config $eavConfig, + array $linkedAttributes = [] + ) { + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->entityType = $entityType; + $this->linkedAttributes = $linkedAttributes; + $this->eavConfig = $eavConfig; + } + + /** + * Form and return query to get eav entity $attributes for given $entityIds. + * + * If eav entities were not found, then data is fetching from $entityTableName. + * + * @param array $entityIds + * @param array $attributes + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + * @throws \Exception + */ + public function getQuery(array $entityIds, array $attributes, int $storeId): Select + { + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->metadataPool->getMetadata($this->entityType); + $entityTableName = $metadata->getEntityTable(); + + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = $this->resourceConnection->getConnection(); + $entityTableAttributes = \array_keys($connection->describeTable($entityTableName)); + + $attributeMetadataTable = $this->resourceConnection->getTableName('eav_attribute'); + $eavAttributes = $this->getEavAttributeCodes($attributes, $entityTableAttributes); + $entityTableAttributes = \array_intersect($attributes, $entityTableAttributes); + + $eavAttributesMetaData = $this->getAttributesMetaData($connection, $attributeMetadataTable, $eavAttributes); + + if ($eavAttributesMetaData) { + $select = $this->getEavAttributes( + $connection, + $metadata, + $entityTableAttributes, + $entityIds, + $eavAttributesMetaData, + $entityTableName, + $storeId + ); + } else { + $select = $this->getAttributesFromEntityTable( + $connection, + $entityTableAttributes, + $entityIds, + $entityTableName + ); + } + + return $select; + } + + /** + * Form and return query to get entity $entityTableAttributes for given $entityIds + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param array $entityTableAttributes + * @param array $entityIds + * @param string $entityTableName + * @return Select + */ + private function getAttributesFromEntityTable( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + array $entityTableAttributes, + array $entityIds, + string $entityTableName + ): Select { + $select = $connection->select() + ->from(['e' => $entityTableName], $entityTableAttributes) + ->where('e.entity_id IN (?)', $entityIds); + + return $select; + } + + /** + * Return ids of eav attributes by $eavAttributeCodes. + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param string $attributeMetadataTable + * @param array $eavAttributeCodes + * @return array + */ + private function getAttributesMetaData( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + string $attributeMetadataTable, + array $eavAttributeCodes + ): array { + $eavAttributeIdsSelect = $connection->select() + ->from(['a' => $attributeMetadataTable], ['attribute_id', 'backend_type', 'attribute_code']) + ->where('a.attribute_code IN (?)', $eavAttributeCodes) + ->where('a.entity_type_id = ?', $this->getEntityTypeId()); + + return $connection->fetchAssoc($eavAttributeIdsSelect); + } + + /** + * Form and return query to get eav entity $attributes for given $entityIds. + * + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param \Magento\Framework\EntityManager\EntityMetadataInterface $metadata + * @param array $entityTableAttributes + * @param array $entityIds + * @param array $eavAttributesMetaData + * @param string $entityTableName + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + */ + private function getEavAttributes( + \Magento\Framework\DB\Adapter\AdapterInterface $connection, + \Magento\Framework\EntityManager\EntityMetadataInterface $metadata, + array $entityTableAttributes, + array $entityIds, + array $eavAttributesMetaData, + string $entityTableName, + int $storeId + ): Select { + $selects = []; + $attributeValueExpression = $connection->getCheckSql( + $connection->getIfNullSql('store_eav.value_id', -1) . ' > 0', + 'store_eav.value', + 'eav.value' + ); + $linkField = $metadata->getLinkField(); + $attributesPerTable = $this->getAttributeCodeTables($entityTableName, $eavAttributesMetaData); + foreach ($attributesPerTable as $attributeTable => $eavAttributes) { + $attributeCodeExpression = $this->buildAttributeCodeExpression($eavAttributes); + + $selects[] = $connection->select() + ->from(['e' => $entityTableName], $entityTableAttributes) + ->joinLeft( + ['eav' => $this->resourceConnection->getTableName($attributeTable)], + \sprintf('e.%1$s = eav.%1$s', $linkField) . + $connection->quoteInto(' AND eav.attribute_id IN (?)', \array_keys($eavAttributesMetaData)) . + $connection->quoteInto(' AND eav.store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID), + [] + ) + ->joinLeft( + ['store_eav' => $this->resourceConnection->getTableName($attributeTable)], + \sprintf( + 'e.%1$s = store_eav.%1$s AND store_eav.attribute_id = ' . + 'eav.attribute_id and store_eav.store_id = %2$d', + $linkField, + $storeId + ), + [] + ) + ->where('e.entity_id IN (?)', $entityIds) + ->columns( + [ + 'attribute_code' => $attributeCodeExpression, + 'value' => $attributeValueExpression + ] + ); + } + + return $connection->select()->union($selects, Select::SQL_UNION_ALL); + } + + /** + * Build expression for attribute code field. + * + * An example: + * + * ``` + * CASE + * WHEN eav.attribute_id = '73' THEN 'name' + * WHEN eav.attribute_id = '121' THEN 'url_key' + * END + * ``` + * + * @param array $eavAttributes + * @return \Zend_Db_Expr + */ + private function buildAttributeCodeExpression(array $eavAttributes): \Zend_Db_Expr + { + $dbConnection = $this->resourceConnection->getConnection(); + $expressionParts = ['CASE']; + + foreach ($eavAttributes as $attribute) { + $expressionParts[]= + $dbConnection->quoteInto('WHEN eav.attribute_id = ?', $attribute['attribute_id'], \Zend_Db::INT_TYPE) . + $dbConnection->quoteInto(' THEN ?', $attribute['attribute_code'], 'string'); + } + + $expressionParts[]= 'END'; + + return new \Zend_Db_Expr(implode(' ', $expressionParts)); + } + + /** + * Get list of attribute tables. + * + * Returns result in the following format: * + * ``` + * $attributeAttributeCodeTables = [ + * 'm2_catalog_product_entity_varchar' => + * '45' => [ + * 'attribute_id' => 45, + * 'backend_type' => 'varchar', + * 'name' => attribute_code, + * ] + * ] + * ]; + * ``` + * + * @param string $entityTable + * @param array $eavAttributesMetaData + * @return array + */ + private function getAttributeCodeTables($entityTable, $eavAttributesMetaData): array + { + $attributeAttributeCodeTables = []; + $metaTypes = \array_unique(\array_column($eavAttributesMetaData, 'backend_type')); + + foreach ($metaTypes as $type) { + if (\in_array($type, self::SUPPORTED_BACKEND_TYPES, true)) { + $tableName = \sprintf('%s_%s', $entityTable, $type); + $attributeAttributeCodeTables[$tableName] = array_filter( + $eavAttributesMetaData, + function ($attribute) use ($type) { + return $attribute['backend_type'] === $type; + } + ); + } + } + + return $attributeAttributeCodeTables; + } + + /** + * Get EAV attribute codes + * Remove attributes from entity table and attributes from exclude list + * Add linked attributes to output + * + * @param array $attributes + * @param array $entityTableAttributes + * @return array + */ + private function getEavAttributeCodes($attributes, $entityTableAttributes): array + { + $attributes = \array_diff($attributes, $entityTableAttributes); + $unusedAttributeList = []; + $newAttributes = []; + foreach ($this->linkedAttributes as $attribute => $linkedAttributes) { + if (null === $linkedAttributes) { + $unusedAttributeList[] = $attribute; + } elseif (\is_array($linkedAttributes) && \in_array($attribute, $attributes, true)) { + $newAttributes[] = $linkedAttributes; + } + } + $attributes = \array_diff($attributes, $unusedAttributeList); + + return \array_unique(\array_merge($attributes, ...$newAttributes)); + } + + /** + * Retrieve entity type id + * + * @return int + * @throws \Exception + */ + private function getEntityTypeId(): int + { + if (!isset($this->entityTypeIdMap[$this->entityType])) { + $this->entityTypeIdMap[$this->entityType] = (int)$this->eavConfig->getEntityType( + $this->metadataPool->getMetadata($this->entityType)->getEavEntityType() + )->getId(); + } + + return $this->entityTypeIdMap[$this->entityType]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php new file mode 100644 index 0000000000000..e3dfa38c78258 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Category\Query; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\DB\Select; + +/** + * Provide category attributes for specified category ids and attributes + */ +class CategoryAttributeQuery +{ + /** + * @var \Magento\CatalogGraphQl\DataProvider\AttributeQueryFactory + */ + private $attributeQueryFactory; + + /** + * @var array + */ + private static $requiredAttributes = [ + 'entity_id', + ]; + + /** + * @param \Magento\CatalogGraphQl\DataProvider\AttributeQueryFactory $attributeQueryFactory + */ + public function __construct( + \Magento\CatalogGraphQl\DataProvider\AttributeQueryFactory $attributeQueryFactory + ) { + $this->attributeQueryFactory = $attributeQueryFactory; + } + + /** + * Form and return query to get eav attributes for given categories + * + * @param array $categoryIds + * @param array $categoryAttributes + * @param int $storeId + * @return Select + * @throws \Zend_Db_Select_Exception + */ + public function getQuery(array $categoryIds, array $categoryAttributes, int $storeId): Select + { + $categoryAttributes = \array_merge($categoryAttributes, self::$requiredAttributes); + + $attributeQuery = $this->attributeQueryFactory->create( + [ + 'entityType' => CategoryInterface::class + ] + ); + + return $attributeQuery->getQuery($categoryIds, $categoryAttributes, $storeId); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php new file mode 100644 index 0000000000000..ea3c0b608d212 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider; + +use Magento\Framework\GraphQl\ConfigInterface; +use Magento\Framework\GraphQl\Config\Element\Type; +use Magento\Framework\GraphQl\Config\Element\InterfaceType; + +/** + * Map for category attributes. + */ +class CategoryAttributesMapper +{ + /** + * @var ConfigInterface + */ + private $graphqlConfig; + + /** + * @param ConfigInterface $graphqlConfig + */ + public function __construct( + ConfigInterface $graphqlConfig + ) { + $this->graphqlConfig = $graphqlConfig; + } + + /** + * Returns attribute values for given attribute codes. + * + * @param array $fetchResult + * @return array + */ + public function getAttributesValues(array $fetchResult): array + { + $attributes = []; + + foreach ($fetchResult as $row) { + if (!isset($attributes[$row['entity_id']])) { + $attributes[$row['entity_id']] = $row; + //TODO: do we need to introduce field mapping? + $attributes[$row['entity_id']]['id'] = $row['entity_id']; + } + if (isset($row['attribute_code'])) { + $attributes[$row['entity_id']][$row['attribute_code']] = $row['value']; + } + } + + return $this->formatAttributes($attributes); + } + + /** + * Format attributes that should be converted to array type + * + * @param array $attributes + * @return array + */ + private function formatAttributes(array $attributes): array + { + $arrayTypeAttributes = $this->getFieldsOfArrayType(); + + return $arrayTypeAttributes + ? array_map( + function ($data) use ($arrayTypeAttributes) { + foreach ($arrayTypeAttributes as $attributeCode) { + $data[$attributeCode] = $this->valueToArray($data[$attributeCode] ?? null); + } + return $data; + }, + $attributes + ) + : $attributes; + } + + /** + * Cast string to array + * + * @param string|null $value + * @return array + */ + private function valueToArray($value): array + { + return $value ? \explode(',', $value) : []; + } + + /** + * Get fields that should be converted to array type + * + * @return array + */ + private function getFieldsOfArrayType(): array + { + $categoryTreeSchema = $this->graphqlConfig->getConfigElement('CategoryTree'); + if (!$categoryTreeSchema instanceof Type) { + throw new \LogicException('CategoryTree type not defined in schema.'); + } + + $fields = []; + foreach ($categoryTreeSchema->getInterfaces() as $interface) { + /** @var InterfaceType $configElement */ + $configElement = $this->graphqlConfig->getConfigElement($interface['interface']); + + foreach ($configElement->getFields() as $field) { + if ($field->isList()) { + $fields[] = $field->getName(); + } + } + } + + return $fields; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php new file mode 100644 index 0000000000000..320e0adc29b9f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; + +use Magento\Framework\App\ResourceConnection; + +/** + * Fetch product attribute option data including attribute info + * Return data in format: + * [ + * attribute_code => [ + * attribute_code => code, + * attribute_label => attribute label, + * option_label => option label, + * options => [option_id => 'option label', ...], + * ] + * ... + * ] + */ +class AttributeOptionProvider +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct(ResourceConnection $resourceConnection) + { + $this->resourceConnection = $resourceConnection; + } + + /** + * Get option data. Return list of attributes with option data + * + * @param array $optionIds + * @param array $attributeCodes + * @return array + * @throws \Zend_Db_Statement_Exception + */ + public function getOptions(array $optionIds, array $attributeCodes = []): array + { + if (!$optionIds) { + return []; + } + + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from( + ['a' => $this->resourceConnection->getTableName('eav_attribute')], + [ + 'attribute_id' => 'a.attribute_id', + 'attribute_code' => 'a.attribute_code', + 'attribute_label' => 'a.frontend_label', + ] + ) + ->joinLeft( + ['options' => $this->resourceConnection->getTableName('eav_attribute_option')], + 'a.attribute_id = options.attribute_id', + [] + ) + ->joinLeft( + ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')], + 'options.option_id = option_value.option_id', + [ + 'option_label' => 'option_value.value', + 'option_id' => 'option_value.option_id', + ] + ); + + $select->where('option_value.option_id IN (?)', $optionIds); + + if (!empty($attributeCodes)) { + $select->orWhere( + 'a.attribute_code in (?) AND a.frontend_input = \'boolean\'', + $attributeCodes + ); + } + + return $this->formatResult($select); + } + + /** + * Format result + * + * @param \Magento\Framework\DB\Select $select + * @return array + * @throws \Zend_Db_Statement_Exception + */ + private function formatResult(\Magento\Framework\DB\Select $select): array + { + $statement = $this->resourceConnection->getConnection()->query($select); + + $result = []; + while ($option = $statement->fetch()) { + if (!isset($result[$option['attribute_code']])) { + $result[$option['attribute_code']] = [ + 'attribute_id' => $option['attribute_id'], + 'attribute_code' => $option['attribute_code'], + 'attribute_label' => $option['attribute_label'], + 'options' => [], + ]; + } + $result[$option['attribute_code']]['options'][$option['option_id']] = $option['option_label']; + } + + return $result; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php new file mode 100644 index 0000000000000..0ec65c88024f2 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder; + +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\AttributeOptionProvider; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface; +use Magento\Framework\Api\Search\AggregationInterface; +use Magento\Framework\Api\Search\AggregationValueInterface; +use Magento\Framework\Api\Search\BucketInterface; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; + +/** + * @inheritdoc + */ +class Attribute implements LayerBuilderInterface +{ + /** + * @var string + * @see \Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Category::CATEGORY_BUCKET + */ + private const PRICE_BUCKET = 'price_bucket'; + + /** + * @var string + * @see \Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Price::PRICE_BUCKET + */ + private const CATEGORY_BUCKET = 'category_bucket'; + + /** + * @var AttributeOptionProvider + */ + private $attributeOptionProvider; + + /** + * @var LayerFormatter + */ + private $layerFormatter; + + /** + * @var array + */ + private $bucketNameFilter = [ + self::PRICE_BUCKET, + self::CATEGORY_BUCKET + ]; + + /** + * @param AttributeOptionProvider $attributeOptionProvider + * @param LayerFormatter $layerFormatter + * @param array $bucketNameFilter + */ + public function __construct( + AttributeOptionProvider $attributeOptionProvider, + LayerFormatter $layerFormatter, + $bucketNameFilter = [] + ) { + $this->attributeOptionProvider = $attributeOptionProvider; + $this->layerFormatter = $layerFormatter; + $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter); + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Zend_Db_Statement_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $attributeOptions = $this->getAttributeOptions($aggregation); + + // build layer per attribute + $result = []; + foreach ($this->getAttributeBuckets($aggregation) as $bucket) { + $bucketName = $bucket->getName(); + $attributeCode = \preg_replace('~_bucket$~', '', $bucketName); + $attribute = $attributeOptions[$attributeCode] ?? []; + + $result[$bucketName] = $this->layerFormatter->buildLayer( + $attribute['attribute_label'] ?? $bucketName, + \count($bucket->getValues()), + $attribute['attribute_code'] ?? $bucketName + ); + + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result[$bucketName]['options'][] = $this->layerFormatter->buildItem( + $attribute['options'][$metrics['value']] ?? $metrics['value'], + $metrics['value'], + $metrics['count'] + ); + } + } + + return $result; + } + + /** + * Get attribute buckets excluding specified bucket names + * + * @param AggregationInterface $aggregation + * @return \Generator|BucketInterface[] + */ + private function getAttributeBuckets(AggregationInterface $aggregation) + { + foreach ($aggregation->getBuckets() as $bucket) { + if (\in_array($bucket->getName(), $this->bucketNameFilter, true)) { + continue; + } + if ($this->isBucketEmpty($bucket)) { + continue; + } + yield $bucket; + } + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } + + /** + * Get list of attributes with options + * + * @param AggregationInterface $aggregation + * @return array + * @throws \Zend_Db_Statement_Exception + */ + private function getAttributeOptions(AggregationInterface $aggregation): array + { + $attributeOptionIds = []; + $attributes = []; + foreach ($this->getAttributeBuckets($aggregation) as $bucket) { + $attributes[] = \preg_replace('~_bucket$~', '', $bucket->getName()); + $attributeOptionIds[] = \array_map( + function (AggregationValueInterface $value) { + return $value->getValue(); + }, + $bucket->getValues() + ); + } + + if (!$attributeOptionIds) { + return []; + } + + return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php new file mode 100644 index 0000000000000..b0e67d72e25ba --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder; + +use Magento\CatalogGraphQl\DataProvider\CategoryAttributesMapper; +use Magento\CatalogGraphQl\DataProvider\Category\Query\CategoryAttributeQuery; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\RootCategoryProvider; +use Magento\Framework\Api\Search\AggregationInterface; +use Magento\Framework\Api\Search\AggregationValueInterface; +use Magento\Framework\Api\Search\BucketInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; + +/** + * @inheritdoc + */ +class Category implements LayerBuilderInterface +{ + /** + * @var string + */ + private const CATEGORY_BUCKET = 'category_bucket'; + + /** + * @var array + */ + private static $bucketMap = [ + self::CATEGORY_BUCKET => [ + 'request_name' => 'category_id', + 'label' => 'Category' + ], + ]; + + /** + * @var CategoryAttributeQuery + */ + private $categoryAttributeQuery; + + /** + * @var CategoryAttributesMapper + */ + private $attributesMapper; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var RootCategoryProvider + */ + private $rootCategoryProvider; + + /** + * @var LayerFormatter + */ + private $layerFormatter; + + /** + * @param CategoryAttributeQuery $categoryAttributeQuery + * @param CategoryAttributesMapper $attributesMapper + * @param RootCategoryProvider $rootCategoryProvider + * @param ResourceConnection $resourceConnection + * @param LayerFormatter $layerFormatter + */ + public function __construct( + CategoryAttributeQuery $categoryAttributeQuery, + CategoryAttributesMapper $attributesMapper, + RootCategoryProvider $rootCategoryProvider, + ResourceConnection $resourceConnection, + LayerFormatter $layerFormatter + ) { + $this->categoryAttributeQuery = $categoryAttributeQuery; + $this->attributesMapper = $attributesMapper; + $this->resourceConnection = $resourceConnection; + $this->rootCategoryProvider = $rootCategoryProvider; + $this->layerFormatter = $layerFormatter; + } + + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Select_Exception + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::CATEGORY_BUCKET); + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $categoryIds = \array_map( + function (AggregationValueInterface $value) { + return (int)$value->getValue(); + }, + $bucket->getValues() + ); + + $categoryIds = \array_diff($categoryIds, [$this->rootCategoryProvider->getRootCategory($storeId)]); + $categoryLabels = \array_column( + $this->attributesMapper->getAttributesValues( + $this->resourceConnection->getConnection()->fetchAll( + $this->categoryAttributeQuery->getQuery($categoryIds, ['name'], $storeId) + ) + ), + 'name', + 'entity_id' + ); + + if (!$categoryLabels) { + return []; + } + + $result = $this->layerFormatter->buildLayer( + self::$bucketMap[self::CATEGORY_BUCKET]['label'], + \count($categoryIds), + self::$bucketMap[self::CATEGORY_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $categoryId = $value->getValue(); + if (!\in_array($categoryId, $categoryIds, true)) { + continue ; + } + $result['options'][] = $this->layerFormatter->buildItem( + $categoryLabels[$categoryId] ?? $categoryId, + $categoryId, + $value->getMetrics()['count'] + ); + } + + return [$result]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php new file mode 100644 index 0000000000000..02b638edbdce8 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder; + +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface; +use Magento\Framework\Api\Search\AggregationInterface; +use Magento\Framework\Api\Search\BucketInterface; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; + +/** + * @inheritdoc + */ +class Price implements LayerBuilderInterface +{ + /** + * @var string + */ + private const PRICE_BUCKET = 'price_bucket'; + + /** + * @var LayerFormatter + */ + private $layerFormatter; + + /** + * @var array + */ + private static $bucketMap = [ + self::PRICE_BUCKET => [ + 'request_name' => 'price', + 'label' => 'Price' + ], + ]; + + /** + * @param LayerFormatter $layerFormatter + */ + public function __construct( + LayerFormatter $layerFormatter + ) { + $this->layerFormatter = $layerFormatter; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $bucket = $aggregation->getBucket(self::PRICE_BUCKET); + if ($this->isBucketEmpty($bucket)) { + return []; + } + + $result = $this->layerFormatter->buildLayer( + self::$bucketMap[self::PRICE_BUCKET]['label'], + \count($bucket->getValues()), + self::$bucketMap[self::PRICE_BUCKET]['request_name'] + ); + + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result['options'][] = $this->layerFormatter->buildItem( + \str_replace('_', '-', $metrics['value']), + $metrics['value'], + $metrics['count'] + ); + } + + return [$result]; + } + + /** + * Check that bucket contains data + * + * @param BucketInterface|null $bucket + * @return bool + */ + private function isBucketEmpty(?BucketInterface $bucket): bool + { + return null === $bucket || !$bucket->getValues(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php new file mode 100644 index 0000000000000..48a1265b10fc3 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter; + +/** + * Format Layered Navigation Items + */ +class LayerFormatter +{ + /** + * Format layer data + * + * @param string $layerName + * @param string $itemsCount + * @param string $requestName + * @return array + */ + public function buildLayer($layerName, $itemsCount, $requestName): array + { + return [ + 'label' => $layerName, + 'count' => $itemsCount, + 'attribute_code' => $requestName + ]; + } + + /** + * Format layer item data + * + * @param string $label + * @param string|int $value + * @param string|int $count + * @return array + */ + public function buildItem($label, $value, $count): array + { + return [ + 'label' => $label, + 'value' => $value, + 'count' => $count, + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php new file mode 100644 index 0000000000000..ff661236be62f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; + +use Magento\Framework\Api\Search\AggregationInterface; + +/** + * @inheritdoc + */ +class LayerBuilder implements LayerBuilderInterface +{ + /** + * @var LayerBuilderInterface[] + */ + private $builders; + + /** + * @param LayerBuilderInterface[] $builders + */ + public function __construct(array $builders) + { + $this->builders = $builders; + } + + /** + * @inheritdoc + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array + { + $layers = []; + foreach ($this->builders as $builder) { + $layers[] = $builder->build($aggregation, $storeId); + } + $layers = \array_merge(...$layers); + + return \array_filter($layers); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php new file mode 100644 index 0000000000000..bd55bc6938b39 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; + +use Magento\Framework\Api\Search\AggregationInterface; + +/** + * Build layer data from AggregationInterface + * Return data in the following format: + * + * [ + * [ + * 'name' => 'layer name', + * 'filter_items_count' => 'filter items count', + * 'request_var' => 'filter name in request', + * 'filter_items' => [ + * 'label' => 'item name', + * 'value_string' => 'item value, e.g. category ID', + * 'items_count' => 'product count', + * ], + * ], + * ... + * ]; + */ +interface LayerBuilderInterface +{ + /** + * Build layer data + * + * @param AggregationInterface $aggregation + * @param int|null $storeId + * @return array [[{layer data}], ...] + */ + public function build(AggregationInterface $aggregation, ?int $storeId): array; +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php new file mode 100644 index 0000000000000..4b8a4a31b3c35 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; + +use Magento\Framework\App\ResourceConnection; + +/** + * Fetch root category id for specified store id + */ +class RootCategoryProvider +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Get root category for specified store id + * + * @param int $storeId + * @return int + */ + public function getRootCategory(int $storeId): int + { + $connection = $this->resourceConnection->getConnection(); + + $select = $connection->select() + ->from( + ['store' => $this->resourceConnection->getTableName('store')], + [] + ) + ->join( + ['store_group' => $this->resourceConnection->getTableName('store_group')], + 'store.group_id = store_group.group_id', + ['root_category_id' => 'store_group.root_category_id'] + ) + ->where('store.store_id = ?', $storeId); + + return (int)$connection->fetchOne($select); + } +} diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php new file mode 100644 index 0000000000000..0e92bbbab4259 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\DataProvider\Product; + +use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Api\SortOrder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Api\SortOrderBuilder; + +/** + * Build search criteria + */ +class SearchCriteriaBuilder +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @var Builder + */ + private $builder; + /** + * @var Visibility + */ + private $visibility; + + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + + /** + * @param Builder $builder + * @param ScopeConfigInterface $scopeConfig + * @param FilterBuilder $filterBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param Visibility $visibility + * @param SortOrderBuilder $sortOrderBuilder + */ + public function __construct( + Builder $builder, + ScopeConfigInterface $scopeConfig, + FilterBuilder $filterBuilder, + FilterGroupBuilder $filterGroupBuilder, + Visibility $visibility, + SortOrderBuilder $sortOrderBuilder + ) { + $this->scopeConfig = $scopeConfig; + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->builder = $builder; + $this->visibility = $visibility; + $this->sortOrderBuilder = $sortOrderBuilder; + } + + /** + * Build search criteria + * + * @param array $args + * @param bool $includeAggregation + * @return SearchCriteriaInterface + */ + public function build(array $args, bool $includeAggregation): SearchCriteriaInterface + { + $searchCriteria = $this->builder->build('products', $args); + $isSearch = !empty($args['search']); + $this->updateRangeFilters($searchCriteria); + + if ($includeAggregation) { + $this->preparePriceAggregation($searchCriteria); + $requestName = 'graphql_product_search_with_aggregation'; + } else { + $requestName = 'graphql_product_search'; + } + $searchCriteria->setRequestName($requestName); + + if ($isSearch) { + $this->addFilter($searchCriteria, 'search_term', $args['search']); + } + + if (!$searchCriteria->getSortOrders()) { + $this->addDefaultSortOrder($searchCriteria, $isSearch); + } + + $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); + + $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setPageSize($args['pageSize']); + + return $searchCriteria; + } + + /** + * Add filter by visibility + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + * @param bool $isFilter + */ + private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bool $isSearch, bool $isFilter): void + { + if ($isFilter && $isSearch) { + // Index already contains products filtered by visibility: catalog, search, both + return ; + } + $visibilityIds = $isSearch + ? $this->visibility->getVisibleInSearchIds() + : $this->visibility->getVisibleInCatalogIds(); + + $this->addFilter($searchCriteria, 'visibility', $visibilityIds); + } + + /** + * Prepare price aggregation algorithm + * + * @param SearchCriteriaInterface $searchCriteria + * @return void + */ + private function preparePriceAggregation(SearchCriteriaInterface $searchCriteria): void + { + $priceRangeCalculation = $this->scopeConfig->getValue( + \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($priceRangeCalculation) { + $this->addFilter($searchCriteria, 'price_dynamic_algorithm', $priceRangeCalculation); + } + } + + /** + * Add filter to search criteria + * + * @param SearchCriteriaInterface $searchCriteria + * @param string $field + * @param mixed $value + */ + private function addFilter(SearchCriteriaInterface $searchCriteria, string $field, $value): void + { + $filter = $this->filterBuilder + ->setField($field) + ->setValue($value) + ->create(); + $this->filterGroupBuilder->addFilter($filter); + $filterGroups = $searchCriteria->getFilterGroups(); + $filterGroups[] = $this->filterGroupBuilder->create(); + $searchCriteria->setFilterGroups($filterGroups); + } + + /** + * Sort by relevance DESC by default + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + */ + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void + { + $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION; + $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC; + $defaultSortOrder = $this->sortOrderBuilder + ->setField($sortField) + ->setDirection($sortDirection) + ->create(); + + $searchCriteria->setSortOrders([$defaultSortOrder]); + } + + /** + * Format range filters so replacement works + * + * Range filter fields in search request must replace value like '%field.from%' or '%field.to%' + * + * @param SearchCriteriaInterface $searchCriteria + */ + private function updateRangeFilters(SearchCriteriaInterface $searchCriteria): void + { + $filterGroups = $searchCriteria->getFilterGroups(); + foreach ($filterGroups as $filterGroup) { + $filters = $filterGroup->getFilters(); + foreach ($filters as $filter) { + if (in_array($filter->getConditionType(), ['from', 'to'])) { + $filter->setField($filter->getField() . '.' . $filter->getConditionType()); + } + } + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php new file mode 100644 index 0000000000000..3a532a1a6c760 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model; + +use \Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolver for aggregation option type. + */ +class AggregationOptionTypeResolver implements TypeResolverInterface +{ + /** + * @inheritdoc + */ + public function resolveType(array $data) : string + { + return isset($data['value']) + && isset($data['label']) + && isset($data['count']) + && count($data) == 3 + ? 'AggregationOption' + : ''; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolverComposite.php b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolverComposite.php new file mode 100644 index 0000000000000..eb0127c63784d --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolverComposite.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model; + +use \Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Composite resolver for aggregation options. + */ +class AggregationOptionTypeResolverComposite implements TypeResolverInterface +{ + /** + * @var TypeResolverInterface[] + */ + private $typeResolvers; + + /** + * @param array $typeResolvers + */ + public function __construct(array $typeResolvers = []) + { + $this->typeResolvers = $typeResolvers; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data) : string + { + /** @var TypeResolverInterface $typeResolver */ + foreach ($this->typeResolvers as $typeResolver) { + $resolvedType = $typeResolver->resolveType($data); + if ($resolvedType) { + return $resolvedType; + } + } + throw new GraphQlInputException(__('Cannot resolve aggregation option type')); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index d57154c429920..b3a9672a47010 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -20,6 +20,24 @@ class AttributesJoiner */ private $queryFields = []; + /** + * Field to attribute mapping + * + * For fields that are not named the same as their attribute, or require extra attributes to resolve + * e.g. ['field' => ['attr1', 'attr2'], 'other_field' => ['other_attr']] + * + * @var array + */ + private $fieldToAttributeMap = []; + + /** + * @param array $fieldToAttributeMap + */ + public function __construct(array $fieldToAttributeMap = []) + { + $this->fieldToAttributeMap = $fieldToAttributeMap; + } + /** * Join fields attached to field node to collection's select. * @@ -30,9 +48,7 @@ class AttributesJoiner public function join(FieldNode $fieldNode, AbstractCollection $collection) : void { foreach ($this->getQueryFields($fieldNode) as $field) { - if (!$collection->isAttributeAdded($field)) { - $collection->addAttributeToSelect($field); - } + $this->addFieldToCollection($collection, $field); } } @@ -42,7 +58,7 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection) : voi * @param FieldNode $fieldNode * @return string[] */ - public function getQueryFields(FieldNode $fieldNode) + public function getQueryFields(FieldNode $fieldNode): array { if (!isset($this->queryFields[$fieldNode->name->value])) { $this->queryFields[$fieldNode->name->value] = []; @@ -58,4 +74,29 @@ public function getQueryFields(FieldNode $fieldNode) return $this->queryFields[$fieldNode->name->value]; } + + /** + * Add field to collection select + * + * Add a query field to the collection, using mapped attribute names if they are set + * + * @param AbstractCollection $collection + * @param string $field + */ + private function addFieldToCollection(AbstractCollection $collection, string $field) + { + $attribute = isset($this->fieldToAttributeMap[$field]) ? $this->fieldToAttributeMap[$field] : $field; + + if (is_array($attribute)) { + foreach ($attribute as $attributeName) { + if (!$collection->isAttributeAdded($attributeName)) { + $collection->addAttributeToSelect($attributeName); + } + } + } else { + if (!$collection->isAttributeAdded($attribute)) { + $collection->addAttributeToSelect($attribute); + } + } + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php new file mode 100644 index 0000000000000..2c03550404ae0 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Category; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Search\Model\Query; + +/** + * Category filter allows to filter collection using 'id, url_key, name' from search criteria. + */ +class CategoryFilter +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Filter for filtering the requested categories id's based on url_key, ids, name in the result. + * + * @param array $args + * @param Collection $categoryCollection + * @param StoreInterface $store + * @throws InputException + */ + public function applyFilters(array $args, Collection $categoryCollection, StoreInterface $store) + { + $categoryCollection->addAttributeToFilter(CategoryInterface::KEY_IS_ACTIVE, ['eq' => 1]); + foreach ($args['filters'] as $field => $cond) { + foreach ($cond as $condType => $value) { + if ($field === 'ids') { + $categoryCollection->addIdFilter($value); + } else { + $this->addAttributeFilter($categoryCollection, $field, $condType, $value, $store); + } + } + } + } + + /** + * Add filter to category collection + * + * @param Collection $categoryCollection + * @param string $field + * @param string $condType + * @param string|array $value + * @param StoreInterface $store + * @throws InputException + */ + private function addAttributeFilter($categoryCollection, $field, $condType, $value, $store) + { + if ($condType === 'match') { + $this->addMatchFilter($categoryCollection, $field, $value, $store); + return; + } + $categoryCollection->addAttributeToFilter($field, [$condType => $value]); + } + + /** + * Add match filter to collection + * + * @param Collection $categoryCollection + * @param string $field + * @param string $value + * @param StoreInterface $store + * @throws InputException + */ + private function addMatchFilter($categoryCollection, $field, $value, $store) + { + $minQueryLength = $this->scopeConfig->getValue( + Query::XML_PATH_MIN_QUERY_LENGTH, + ScopeInterface::SCOPE_STORE, + $store + ); + $searchValue = str_replace('%', '', $value); + $matchLength = strlen($searchValue); + if ($matchLength < $minQueryLength) { + throw new InputException(__('Invalid match filter')); + } + + $categoryCollection->addAttributeToFilter($field, ['like' => "%{$searchValue}%"]); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php index 0ca72d9ff9519..cd11c3c68f794 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php @@ -51,16 +51,24 @@ class CategoryAttributeReader implements ReaderInterface */ private $collectionFactory; + /** + * @var array + */ + private $categoryAttributeResolvers; + /** * @param Type $typeLocator * @param CollectionFactory $collectionFactory + * @param array $categoryAttributeResolvers */ public function __construct( Type $typeLocator, - CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + array $categoryAttributeResolvers = [] ) { $this->typeLocator = $typeLocator; $this->collectionFactory = $collectionFactory; + $this->categoryAttributeResolvers = $categoryAttributeResolvers; } /** @@ -93,6 +101,9 @@ public function read($scope = null) : array $data['fields'][$attributeCode]['name'] = $attributeCode; $data['fields'][$attributeCode]['type'] = $locatedType; $data['fields'][$attributeCode]['arguments'] = []; + if (isset($this->categoryAttributeResolvers[$attributeCode])) { + $data['fields'][$attributeCode]['resolver'] = $this->categoryAttributeResolvers[$attributeCode]; + } } $config['CategoryInterface'] = $data; diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php new file mode 100644 index 0000000000000..4f3a88cc788df --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Config; + +use Magento\Framework\Config\ReaderInterface; +use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; + +/** + * Adds custom/eav attributes to product filter type in the GraphQL config. + * + * Product Attribute should satisfy the following criteria: + * - Attribute is searchable + * - "Visible in Advanced Search" is set to "Yes" + * - Attribute of type "Select" must have options + */ +class FilterAttributeReader implements ReaderInterface +{ + /** + * Entity type constant + */ + private const ENTITY_TYPE = 'filter_attributes'; + + /** + * Filter input types + */ + private const FILTER_EQUAL_TYPE = 'FilterEqualTypeInput'; + private const FILTER_RANGE_TYPE = 'FilterRangeTypeInput'; + private const FILTER_MATCH_TYPE = 'FilterMatchTypeInput'; + + /** + * @var MapperInterface + */ + private $mapper; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var array + */ + private $exactMatchAttributes = ['sku']; + + /** + * @param MapperInterface $mapper + * @param CollectionFactory $collectionFactory + * @param array $exactMatchAttributes + */ + public function __construct( + MapperInterface $mapper, + CollectionFactory $collectionFactory, + array $exactMatchAttributes = [] + ) { + $this->mapper = $mapper; + $this->collectionFactory = $collectionFactory; + $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes); + } + + /** + * Read configuration scope + * + * @param string|null $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function read($scope = null) : array + { + $typeNames = $this->mapper->getMappedTypes(self::ENTITY_TYPE); + $config = []; + + foreach ($this->getAttributeCollection() as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + + foreach ($typeNames as $typeName) { + $config[$typeName]['fields'][$attributeCode] = [ + 'name' => $attributeCode, + 'type' => $this->getFilterType($attribute), + 'arguments' => [], + 'required' => false, + 'description' => sprintf('Attribute label: %s', $attribute->getDefaultFrontendLabel()) + ]; + } + } + + return $config; + } + + /** + * Map attribute type to filter type + * + * @param Attribute $attribute + * @return string + */ + private function getFilterType(Attribute $attribute): string + { + if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) { + return self::FILTER_EQUAL_TYPE; + } + + $filterTypeMap = [ + 'price' => self::FILTER_RANGE_TYPE, + 'date' => self::FILTER_RANGE_TYPE, + 'select' => self::FILTER_EQUAL_TYPE, + 'multiselect' => self::FILTER_EQUAL_TYPE, + 'boolean' => self::FILTER_EQUAL_TYPE, + 'text' => self::FILTER_MATCH_TYPE, + 'textarea' => self::FILTER_MATCH_TYPE, + ]; + + return $filterTypeMap[$attribute->getFrontendInput()] ?? self::FILTER_MATCH_TYPE; + } + + /** + * Create attribute collection + * + * @return Collection|\Magento\Catalog\Model\ResourceModel\Eav\Attribute[] + */ + private function getAttributeCollection() + { + return $this->collectionFactory->create() + ->addHasOptionsFilter() + ->addIsSearchableFilter() + ->addDisplayInAdvancedSearchFilter(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php new file mode 100644 index 0000000000000..215b28be0579c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogGraphQl\Model\Config; + +use Magento\Framework\Config\ReaderInterface; +use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection; + +/** + * Adds custom/eav attribute to catalog products sorting in the GraphQL config. + */ +class SortAttributeReader implements ReaderInterface +{ + /** + * Entity type constant + */ + private const ENTITY_TYPE = 'sort_attributes'; + + /** + * Fields type constant + */ + private const FIELD_TYPE = 'SortEnum'; + + /** + * @var MapperInterface + */ + private $mapper; + + /** + * @var AttributesCollection + */ + private $attributesCollection; + + /** + * @param MapperInterface $mapper + * @param AttributesCollection $attributesCollection + */ + public function __construct( + MapperInterface $mapper, + AttributesCollection $attributesCollection + ) { + $this->mapper = $mapper; + $this->attributesCollection = $attributesCollection; + } + + /** + * Read configuration scope + * + * @param string|null $scope + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function read($scope = null) : array + { + $map = $this->mapper->getMappedTypes(self::ENTITY_TYPE); + $config =[]; + $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + foreach ($attributes as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + $attributeLabel = $attribute->getDefaultFrontendLabel(); + foreach ($map as $type) { + $config[$type]['fields'][$attributeCode] = [ + 'name' => $attributeCode, + 'type' => self::FIELD_TYPE, + 'arguments' => [], + 'required' => false, + 'description' => __('Attribute label: ') . $attributeLabel + ]; + } + } + + return $config; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php index e1106a3f696e4..cd582ffda9244 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php +++ b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php @@ -10,9 +10,12 @@ use Magento\Catalog\Model\Product\Option\Type\Date as ProductDateOptionType; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** - * @inheritdoc + * CatalogGraphQl product option date type + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class DateType extends ProductDateOptionType { @@ -43,6 +46,13 @@ private function formatValues($values) if (isset($values[$this->getOption()->getId()])) { $value = $values[$this->getOption()->getId()]; $dateTime = \DateTime::createFromFormat(DateTime::DATETIME_PHP_FORMAT, $value); + + if ($dateTime === false) { + throw new GraphQlInputException( + __('Invalid format provided. Please use \'Y-m-d H:i:s\' format.') + ); + } + $values[$this->getOption()->getId()] = [ 'date' => $value, 'year' => $dateTime->format('Y'), diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php new file mode 100644 index 0000000000000..47a1d1f977f9b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Layered navigation filters resolver, used for GraphQL request processing. + */ +class Aggregations implements ResolverInterface +{ + /** + * @var Layer\DataProvider\Filters + */ + private $filtersDataProvider; + + /** + * @var LayerBuilder + */ + private $layerBuilder; + + /** + * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + * @param LayerBuilder $layerBuilder + */ + public function __construct( + \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, + LayerBuilder $layerBuilder + ) { + $this->filtersDataProvider = $filtersDataProvider; + $this->layerBuilder = $layerBuilder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['layer_type']) || !isset($value['search_result'])) { + return null; + } + + $aggregations = $value['search_result']->getSearchAggregation(); + + if ($aggregations) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->layerBuilder->build($aggregations, $storeId); + } else { + return []; + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CanonicalUrl.php new file mode 100644 index 0000000000000..3cce413ff93f3 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CanonicalUrl.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Category; + +use Magento\Catalog\Model\Category; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Catalog\Helper\Category as CategoryHelper; + +/** + * Resolve data for category canonical URL + */ +class CanonicalUrl implements ResolverInterface +{ + /** @var CategoryHelper */ + private $categoryHelper; + + /** + * CanonicalUrl constructor. + * @param CategoryHelper $categoryHelper + */ + public function __construct(CategoryHelper $categoryHelper) + { + $this->categoryHelper = $categoryHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /* @var Category $category */ + $category = $value['model']; + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + if ($this->categoryHelper->canUseCanonicalTag($store)) { + $baseUrl = $category->getUrlInstance()->getBaseUrl(); + return str_replace($baseUrl, '', $category->getUrl()); + } + return null; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php index 9e23c4f1e9736..863e621bd8df3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php @@ -29,8 +29,11 @@ public function __construct( } /** + * Get breadcrumbs data + * * @param string $categoryPath * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getData(string $categoryPath): array { @@ -41,7 +44,7 @@ public function getData(string $categoryPath): array if (count($parentCategoryIds)) { $collection = $this->collectionFactory->create(); - $collection->addAttributeToSelect(['name', 'url_key']); + $collection->addAttributeToSelect(['name', 'url_key', 'url_path']); $collection->addAttributeToFilter('entity_id', $parentCategoryIds); foreach ($collection as $category) { @@ -50,6 +53,7 @@ public function getData(string $categoryPath): array 'category_name' => $category->getName(), 'category_level' => $category->getLevel(), 'category_url_key' => $category->getUrlKey(), + 'category_url_path' => $category->getUrlPath(), ]; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php new file mode 100644 index 0000000000000..a06a8252d5a5e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Category; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Exception\LocalizedException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\Filesystem\DirectoryList; + +/** + * Resolve category image to a fully qualified URL + */ +class Image implements ResolverInterface +{ + /** @var DirectoryList */ + private $directoryList; + + /** + * @param DirectoryList $directoryList + */ + public function __construct(DirectoryList $directoryList) + { + $this->directoryList = $directoryList; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var \Magento\Catalog\Model\Category $category */ + $category = $value['model']; + $imagePath = $category->getImage(); + if (empty($imagePath)) { + return null; + } + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $baseUrl = $store->getBaseUrl('media'); + + $mediaPath = $this->directoryList->getUrlPath('media'); + $pos = strpos($imagePath, $mediaPath); + if ($pos !== false) { + $imagePath = substr($imagePath, $pos + strlen($mediaPath), strlen($baseUrl)); + } + $imageUrl = rtrim($baseUrl, '/') . '/' . ltrim($imagePath, '/'); + + return $imageUrl; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index e0580213ddea7..abc5ae7e1da7f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; +use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -27,27 +30,46 @@ class Products implements ResolverInterface /** * @var Builder + * @deprecated */ private $searchCriteriaBuilder; /** * @var Filter + * @deprecated */ private $filterQuery; + /** + * @var Search + */ + private $searchQuery; + + /** + * @var SearchCriteriaBuilder + */ + private $searchApiCriteriaBuilder; + /** * @param ProductRepositoryInterface $productRepository * @param Builder $searchCriteriaBuilder * @param Filter $filterQuery + * @param Search $searchQuery + * @param SearchCriteriaBuilder $searchApiCriteriaBuilder */ public function __construct( ProductRepositoryInterface $productRepository, Builder $searchCriteriaBuilder, - Filter $filterQuery + Filter $filterQuery, + Search $searchQuery = null, + SearchCriteriaBuilder $searchApiCriteriaBuilder = null ) { $this->productRepository = $productRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterQuery = $filterQuery; + $this->searchQuery = $searchQuery ?? ObjectManager::getInstance()->get(Search::class); + $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ?? + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -60,21 +82,20 @@ public function resolve( array $value = null, array $args = null ) { - $args['filter'] = [ - 'category_id' => [ - 'eq' => $value['id'] - ] - ]; - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } if ($args['pageSize'] < 1) { throw new GraphQlInputException(__('pageSize value must be greater than 0.')); } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); + + $args['filter'] = [ + 'category_id' => [ + 'eq' => $value['id'] + ] + ]; + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, false); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info); //possible division by 0 if ($searchCriteria->getPageSize()) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php new file mode 100644 index 0000000000000..6b8949d612829 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver; + +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; +use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; +use Magento\CatalogGraphQl\Model\Category\CategoryFilter; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; + +/** + * Category List resolver, used for GraphQL category data request processing. + */ +class CategoryList implements ResolverInterface +{ + /** + * @var CategoryTree + */ + private $categoryTree; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var CategoryFilter + */ + private $categoryFilter; + + /** + * @var ExtractDataFromCategoryTree + */ + private $extractDataFromCategoryTree; + + /** + * @param CategoryTree $categoryTree + * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree + * @param CategoryFilter $categoryFilter + * @param CollectionFactory $collectionFactory + */ + public function __construct( + CategoryTree $categoryTree, + ExtractDataFromCategoryTree $extractDataFromCategoryTree, + CategoryFilter $categoryFilter, + CollectionFactory $collectionFactory + ) { + $this->categoryTree = $categoryTree; + $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; + $this->categoryFilter = $categoryFilter; + $this->collectionFactory = $collectionFactory; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (isset($value[$field->getName()])) { + return $value[$field->getName()]; + } + $store = $context->getExtensionAttributes()->getStore(); + + $rootCategoryIds = []; + if (!isset($args['filters'])) { + $rootCategoryIds[] = (int)$store->getRootCategoryId(); + } else { + $categoryCollection = $this->collectionFactory->create(); + try { + $this->categoryFilter->applyFilters($args, $categoryCollection, $store); + } catch (InputException $e) { + return []; + } + + foreach ($categoryCollection as $category) { + $rootCategoryIds[] = (int)$category->getId(); + } + } + + $result = $this->fetchCategories($rootCategoryIds, $info); + return $result; + } + + /** + * Fetch category tree data + * + * @param array $categoryIds + * @param ResolveInfo $info + * @return array + * @throws GraphQlNoSuchEntityException + */ + private function fetchCategories(array $categoryIds, ResolveInfo $info) + { + $fetchedCategories = []; + foreach ($categoryIds as $categoryId) { + $categoryTree = $this->categoryTree->getTree($info, $categoryId); + if (empty($categoryTree)) { + continue; + } + $fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree)); + } + + return $fetchedCategories; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php index 89d3805383e1a..4284aed610848 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php @@ -10,11 +10,11 @@ use Magento\Catalog\Model\Category; use Magento\CatalogGraphQl\Model\Resolver\Category\CheckCategoryIsActive; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree as CategoryTreeDataProvider; /** * Category tree field resolver, used for GraphQL request processing. @@ -27,7 +27,7 @@ class CategoryTree implements ResolverInterface const CATEGORY_INTERFACE = 'CategoryInterface'; /** - * @var \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree + * @var CategoryTreeDataProvider */ private $categoryTree; @@ -42,12 +42,12 @@ class CategoryTree implements ResolverInterface private $checkCategoryIsActive; /** - * @param \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree + * @param CategoryTreeDataProvider $categoryTree * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree * @param CheckCategoryIsActive $checkCategoryIsActive */ public function __construct( - \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree, + CategoryTreeDataProvider $categoryTree, ExtractDataFromCategoryTree $extractDataFromCategoryTree, CheckCategoryIsActive $checkCategoryIsActive ) { @@ -56,22 +56,6 @@ public function __construct( $this->checkCategoryIsActive = $checkCategoryIsActive; } - /** - * Get category id - * - * @param array $args - * @return int - * @throws GraphQlInputException - */ - private function getCategoryId(array $args) : int - { - if (!isset($args['id'])) { - throw new GraphQlInputException(__('"id for category should be specified')); - } - - return (int)$args['id']; - } - /** * @inheritdoc */ @@ -81,7 +65,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return $value[$field->getName()]; } - $rootCategoryId = $this->getCategoryId($args); + $rootCategoryId = isset($args['id']) ? (int)$args['id'] : + (int)$context->getExtensionAttributes()->getStore()->getRootCategoryId(); + if ($rootCategoryId !== Category::TREE_ROOT_ID) { $this->checkCategoryIsActive->execute($rootCategoryId); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php index 786d4f1ab867c..f6d8edf1fe9b5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Layer\Filter\AbstractFilter; use Magento\CatalogGraphQl\Model\Resolver\Layer\FiltersProvider; +use Magento\Catalog\Model\Layer\Filter\Item; /** * Layered navigation filters data provider. @@ -20,6 +21,11 @@ class Filters */ private $filtersProvider; + /** + * @var array + */ + private $mappings; + /** * Filters constructor. * @param FiltersProvider $filtersProvider @@ -28,26 +34,31 @@ public function __construct( FiltersProvider $filtersProvider ) { $this->filtersProvider = $filtersProvider; + $this->mappings = [ + 'Category' => 'category' + ]; } /** * Get layered navigation filters data * * @param string $layerType + * @param array|null $attributesToFilter * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getData(string $layerType) : array + public function getData(string $layerType, array $attributesToFilter = null) : array { $filtersData = []; /** @var AbstractFilter $filter */ foreach ($this->filtersProvider->getFilters($layerType) as $filter) { - if ($filter->getItemsCount()) { + if ($this->isNeedToAddFilter($filter, $attributesToFilter)) { $filterGroup = [ 'name' => (string)$filter->getName(), 'filter_items_count' => $filter->getItemsCount(), 'request_var' => $filter->getRequestVar(), ]; - /** @var \Magento\Catalog\Model\Layer\Filter\Item $filterItem */ + /** @var Item $filterItem */ foreach ($filter->getItems() as $filterItem) { $filterGroup['filter_items'][] = [ 'label' => (string)$filterItem->getLabel(), @@ -60,4 +71,32 @@ public function getData(string $layerType) : array } return $filtersData; } + + /** + * Check for adding filter to the list + * + * @param AbstractFilter $filter + * @param array $attributesToFilter + * @return bool + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function isNeedToAddFilter(AbstractFilter $filter, array $attributesToFilter): bool + { + if ($attributesToFilter === null) { + $result = (bool)$filter->getItemsCount(); + } else { + if ($filter->hasAttributeModel()) { + $filterAttribute = $filter->getAttributeModel(); + $result = in_array($filterAttribute->getAttributeCode(), $attributesToFilter); + } else { + $name = (string)$filter->getName(); + if (array_key_exists($name, $this->mappings)) { + $result = in_array($this->mappings[$name], $attributesToFilter); + } else { + $result = true; + } + } + } + return $result; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php index 0ec7e12e42d55..78ac45a1ad001 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php @@ -44,6 +44,29 @@ public function resolve( return null; } - return $this->filtersDataProvider->getData($value['layer_type']); + $attributes = $this->prepareAttributesResults($value); + return $this->filtersDataProvider->getData($value['layer_type'], $attributes); + } + + /** + * Get attributes available to filtering from the search result + * + * @param array $value + * @return array|null + */ + private function prepareAttributesResults(array $value): ?array + { + $attributes = []; + if (!empty($value['search_result'])) { + $buckets = $value['search_result']->getSearchAggregation()->getBuckets(); + foreach ($buckets as $bucket) { + if (!empty($bucket->getValues())) { + $attributes[] = str_replace('_bucket', '', $bucket->getName()); + } + } + } else { + $attributes = null; + } + return $attributes; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php index 86137990cc57d..889735a5f4d88 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php @@ -14,6 +14,7 @@ use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Exception\LocalizedException; /** * @inheritdoc @@ -63,10 +64,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $this->productDataProvider->addEavAttributes($fields); $result = function () use ($value) { - $data = $this->productDataProvider->getProductBySku($value['sku']); + $data = $value['product'] ?? $this->productDataProvider->getProductBySku($value['sku']); if (empty($data)) { return null; } + if (!isset($data['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } $productModel = $data['model']; /** @var \Magento\Catalog\Model\Product $productModel */ $data = $productModel->getData(); @@ -79,10 +83,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } } } - return array_replace($value, $data); }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php new file mode 100644 index 0000000000000..14732ecf37c63 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Catalog\Model\ProductLink\Data\ListCriteria; +use Magento\Catalog\Model\ProductLink\ProductLinkQuery; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\BatchServiceContractResolverInterface; +use Magento\Framework\GraphQl\Query\Resolver\ResolveRequestInterface; +use Magento\Catalog\Api\Data\ProductLinkInterface; + +/** + * Format the product links information to conform to GraphQL schema representation + */ +class BatchProductLinks implements BatchServiceContractResolverInterface +{ + /** + * @var string[] + */ + private static $linkTypes = ['related', 'upsell', 'crosssell']; + + /** + * @inheritDoc + */ + public function getServiceContract(): array + { + return [ProductLinkQuery::class, 'search']; + } + + /** + * @inheritDoc + */ + public function convertToServiceArgument(ResolveRequestInterface $request) + { + $value = $request->getValue(); + if (empty($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var \Magento\Catalog\Model\Product $product */ + $product = $value['model']; + + return new ListCriteria((string)$product->getId(), self::$linkTypes, $product); + } + + /** + * @inheritDoc + */ + public function convertFromServiceResult($result, ResolveRequestInterface $request) + { + /** @var \Magento\Catalog\Model\ProductLink\Data\ListResultInterface $result */ + if ($result->getError()) { + //If model isn't there previous method would've thrown an exception. + /** @var \Magento\Catalog\Model\Product $product */ + $product = $request->getValue()['model']; + throw new LocalizedException( + __('Failed to retrieve product links for "%1"', $product->getSku()), + $result->getError() + ); + } + + return array_filter( + array_map( + function (ProductLinkInterface $link) { + return [ + 'sku' => $link->getSku(), + 'link_type' => $link->getLinkType(), + 'linked_product_sku' => $link->getLinkedProductSku(), + 'linked_product_type' => $link->getLinkedProductType(), + 'position' => $link->getPosition() + ]; + }, + $result->getResult() + ) + ); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php index 9047eaee4b568..0f764d1daa5e7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php @@ -12,12 +12,25 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Store\Api\Data\StoreInterface; /** * Resolve data for product canonical URL */ class CanonicalUrl implements ResolverInterface { + /** @var ProductHelper */ + private $productHelper; + + /** + * @param Product $productHelper + */ + public function __construct(ProductHelper $productHelper) + { + $this->productHelper = $productHelper; + } + /** * @inheritdoc */ @@ -32,10 +45,14 @@ public function resolve( throw new LocalizedException(__('"model" value should be specified')); } - /* @var $product Product */ + /* @var Product $product */ $product = $value['model']; - $url = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]); - - return $url; + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + if ($this->productHelper->canUseCanonicalTag($store)) { + $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]); + return $product->getRequestPath(); + } + return null; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php deleted file mode 100644 index 4ec76fe59ca88..0000000000000 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Store\Api\Data\StoreInterface; - -/** - * Return media label - */ -class Label implements ResolverInterface -{ - /** - * @var ProductResourceModel - */ - private $productResource; - - /** - * @param ProductResourceModel $productResource - */ - public function __construct( - ProductResourceModel $productResource - ) { - $this->productResource = $productResource; - } - - /** - * @inheritdoc - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - array $value = null, - array $args = null - ) { - - if (isset($value['label'])) { - return $value['label']; - } - - if (!isset($value['model'])) { - throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ - $product = $value['model']; - $productId = (int)$product->getEntityId(); - /** @var StoreInterface $store */ - $store = $context->getExtensionAttributes()->getStore(); - $storeId = (int)$store->getId(); - if (!isset($value['image_type'])) { - return $this->getAttributeValue($productId, 'name', $storeId); - } - $imageType = $value['image_type']; - $imageLabel = $this->getAttributeValue($productId, $imageType . '_label', $storeId); - if ($imageLabel == null) { - $imageLabel = $this->getAttributeValue($productId, 'name', $storeId); - } - - return $imageLabel; - } - - /** - * Get attribute value - * - * @param int $productId - * @param string $attributeCode - * @param int $storeId - * @return null|string Null if attribute value is not exists - */ - private function getAttributeValue(int $productId, string $attributeCode, int $storeId): ?string - { - $value = $this->productResource->getAttributeRawValue($productId, $attributeCode, $storeId); - return is_array($value) && empty($value) ? null : $value; - } -} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php index eaab159cddae6..359d295095667 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php @@ -24,11 +24,17 @@ class Url implements ResolverInterface * @var ImageFactory */ private $productImageFactory; + /** * @var PlaceholderProvider */ private $placeholderProvider; + /** + * @var string[] + */ + private $placeholderCache = []; + /** * @param ImageFactory $productImageFactory * @param PlaceholderProvider $placeholderProvider @@ -64,12 +70,8 @@ public function resolve( if (isset($value['image_type'])) { $imagePath = $product->getData($value['image_type']); return $this->getImageUrl($value['image_type'], $imagePath); - } - if (isset($value['file'])) { - $image = $this->productImageFactory->create(); - $image->setDestinationSubdir('image')->setBaseFile($value['file']); - $imageUrl = $image->getUrl(); - return $imageUrl; + } elseif (isset($value['file'])) { + return $this->getImageUrl('image', $value['file']); } return []; } @@ -84,12 +86,16 @@ public function resolve( */ private function getImageUrl(string $imageType, ?string $imagePath): string { + if (empty($imagePath) && !empty($this->placeholderCache[$imageType])) { + return $this->placeholderCache[$imageType]; + } $image = $this->productImageFactory->create(); $image->setDestinationSubdir($imageType) ->setBaseFile($imagePath); if ($image->isBaseFilePlaceholder()) { - return $this->placeholderProvider->getPlaceholder($imageType); + $this->placeholderCache[$imageType] = $this->placeholderProvider->getPlaceholder($imageType); + return $this->placeholderCache[$imageType]; } return $image->getUrl(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php new file mode 100644 index 0000000000000..c56e05bf267a4 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +/** + * Calculate price discount as value and percent + */ +class Discount +{ + /** + * @var float + */ + private $zeroThreshold = 0.0001; + + /** + * Get formatted discount between two prices + * + * @param float $regularPrice + * @param float $finalPrice + * @return array + */ + public function getDiscountByDifference(float $regularPrice, float $finalPrice): array + { + return [ + 'amount_off' => $this->getPriceDifferenceAsValue($regularPrice, $finalPrice), + 'percent_off' => $this->getPriceDifferenceAsPercent($regularPrice, $finalPrice) + ]; + } + + /** + * Get formatted discount based on percent off + * + * @param float $regularPrice + * @param float $percentOff + * @return array + */ + public function getDiscountByPercent(float $regularPrice, float $percentOff): array + { + return [ + 'amount_off' => $this->getPercentDiscountAsValue($regularPrice, $percentOff), + 'percent_off' => $percentOff + ]; + } + + /** + * Get value difference between two prices + * + * @param float $regularPrice + * @param float $finalPrice + * @return float + */ + private function getPriceDifferenceAsValue(float $regularPrice, float $finalPrice): float + { + $difference = $regularPrice - $finalPrice; + if ($difference <= $this->zeroThreshold) { + return 0; + } + return round($difference, 2); + } + + /** + * Get percent difference between two prices + * + * @param float $regularPrice + * @param float $finalPrice + * @return float + */ + private function getPriceDifferenceAsPercent(float $regularPrice, float $finalPrice): float + { + $difference = $this->getPriceDifferenceAsValue($regularPrice, $finalPrice); + + if ($difference <= $this->zeroThreshold || $regularPrice <= $this->zeroThreshold) { + return 0; + } + + return round(($difference / $regularPrice) * 100, 2); + } + + /** + * Get amount difference that percentOff represents + * + * @param float $regularPrice + * @param float $percentOff + * @return float + */ + private function getPercentDiscountAsValue(float $regularPrice, float $percentOff): float + { + $percentDecimal = $percentOff / 100; + $valueDiscount = $regularPrice * $percentDecimal; + + return round($valueDiscount, 2); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..67dbcf861170f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; + +/** + * Provides product prices + */ +class Provider implements ProviderInterface +{ + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + /** @var FinalPrice $finalPrice */ + $finalPrice = $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE); + return $finalPrice->getMinimalPrice(); + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + return $this->getRegularPrice($product); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + /** @var FinalPrice $finalPrice */ + $finalPrice = $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE); + return $finalPrice->getMaximalPrice(); + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + return $this->getRegularPrice($product); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php new file mode 100644 index 0000000000000..99459daf045a5 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; + +/** + * Provides product prices + */ +interface ProviderInterface +{ + /** + * Get the product minimal final price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product minimal regular price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product maximum final price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product maximum final price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product regular price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface; +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderPool.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderPool.php new file mode 100644 index 0000000000000..a23c28a868b6c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderPool.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +/** + * Pool of price providers for different product types + */ +class ProviderPool +{ + private const DEFAULT = 'default'; + + /** + * @var ProviderInterface[] + */ + private $providers; + + /** + * @param ProviderInterface[] $providers + */ + public function __construct(array $providers) + { + $this->providers = $providers; + } + + /** + * Get price provider by product type + * + * @param string $productType + * @return ProviderInterface + */ + public function getProviderByProductType(string $productType): ProviderInterface + { + if (isset($this->providers[$productType])) { + return $this->providers[$productType]; + } + return $this->providers[self::DEFAULT]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php new file mode 100644 index 0000000000000..9396b1f02b975 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Format product's pricing information for price_range field + */ +class PriceRange implements ResolverInterface +{ + /** + * @var Discount + */ + private $discount; + + /** + * @var PriceProviderPool + */ + private $priceProviderPool; + + /** + * @param PriceProviderPool $priceProviderPool + * @param Discount $discount + */ + public function __construct(PriceProviderPool $priceProviderPool, Discount $discount) + { + $this->priceProviderPool = $priceProviderPool; + $this->discount = $discount; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var Product $product */ + $product = $value['model']; + $product->unsetData('minimal_price'); + + $requestedFields = $info->getFieldSelection(10); + $returnArray = []; + + if (isset($requestedFields['minimum_price'])) { + $returnArray['minimum_price'] = $this->getMinimumProductPrice($product, $store); + } + if (isset($requestedFields['maximum_price'])) { + $returnArray['maximum_price'] = $this->getMaximumProductPrice($product, $store); + } + return $returnArray; + } + + /** + * Get formatted minimum product price + * + * @param SaleableInterface $product + * @param StoreInterface $store + * @return array + */ + private function getMinimumProductPrice(SaleableInterface $product, StoreInterface $store): array + { + $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId()); + $regularPrice = $priceProvider->getMinimalRegularPrice($product)->getValue(); + $finalPrice = $priceProvider->getMinimalFinalPrice($product)->getValue(); + $minPriceArray = $this->formatPrice($regularPrice, $finalPrice, $store); + $minPriceArray['model'] = $product; + return $minPriceArray; + } + + /** + * Get formatted maximum product price + * + * @param SaleableInterface $product + * @param StoreInterface $store + * @return array + */ + private function getMaximumProductPrice(SaleableInterface $product, StoreInterface $store): array + { + $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId()); + $regularPrice = $priceProvider->getMaximalRegularPrice($product)->getValue(); + $finalPrice = $priceProvider->getMaximalFinalPrice($product)->getValue(); + $maxPriceArray = $this->formatPrice($regularPrice, $finalPrice, $store); + $maxPriceArray['model'] = $product; + return $maxPriceArray; + } + + /** + * Format price for GraphQl output + * + * @param float $regularPrice + * @param float $finalPrice + * @param StoreInterface $store + * @return array + */ + private function formatPrice(float $regularPrice, float $finalPrice, StoreInterface $store): array + { + return [ + 'regular_price' => [ + 'value' => $regularPrice, + 'currency' => $store->getCurrentCurrencyCode() + ], + 'final_price' => [ + 'value' => $finalPrice, + 'currency' => $store->getCurrentCurrencyCode() + ], + 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice), + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php index d1566162472b0..7c08f91c922bd 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php @@ -18,6 +18,25 @@ */ class ProductImage implements ResolverInterface { + /** @var array */ + private static $catalogImageLabelTypes = [ + 'image' => 'image_label', + 'small_image' => 'small_image_label', + 'thumbnail' => 'thumbnail_label' + ]; + + /** @var array */ + private $imageTypeLabels; + + /** + * @param array $imageTypeLabels + */ + public function __construct( + array $imageTypeLabels = [] + ) { + $this->imageTypeLabels = array_replace(self::$catalogImageLabelTypes, $imageTypeLabels); + } + /** * @inheritdoc */ @@ -34,11 +53,16 @@ public function resolve( /** @var Product $product */ $product = $value['model']; - $imageType = $field->getName(); + $label = $value['name'] ?? null; + if (isset($this->imageTypeLabels[$info->fieldName]) + && !empty($value[$this->imageTypeLabels[$info->fieldName]])) { + $label = $value[$this->imageTypeLabels[$info->fieldName]]; + } return [ 'model' => $product, - 'image_type' => $imageType, + 'image_type' => $field->getName(), + 'label' => $label ]; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php deleted file mode 100644 index 726ef91c56880..0000000000000 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\TierPrice; -use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; - -/** - * @inheritdoc - * - * Format a product's tier price information to conform to GraphQL schema representation - */ -class TierPrices implements ResolverInterface -{ - /** - * @inheritdoc - * - * Format product's tier price data to conform to GraphQL schema - * - * @param \Magento\Framework\GraphQl\Config\Element\Field $field - * @param ContextInterface $context - * @param ResolveInfo $info - * @param array|null $value - * @param array|null $args - * @throws \Exception - * @return null|array - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - array $value = null, - array $args = null - ) { - if (!isset($value['model'])) { - throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ - $product = $value['model']; - - $tierPrices = null; - if ($product->getTierPrices()) { - $tierPrices = []; - /** @var TierPrice $tierPrice */ - foreach ($product->getTierPrices() as $tierPrice) { - $tierPrices[] = $tierPrice->getData(); - } - } - - return $tierPrices; - } -} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index a75a9d2cf50a0..691f93e4148bc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -16,6 +16,7 @@ use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Catalog\Model\Layer\Resolver; +use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; /** * Products field resolver, used for GraphQL request processing. @@ -24,6 +25,7 @@ class Products implements ResolverInterface { /** * @var Builder + * @deprecated */ private $searchCriteriaBuilder; @@ -34,30 +36,41 @@ class Products implements ResolverInterface /** * @var Filter + * @deprecated */ private $filterQuery; /** * @var SearchFilter + * @deprecated */ private $searchFilter; + /** + * @var SearchCriteriaBuilder + */ + private $searchApiCriteriaBuilder; + /** * @param Builder $searchCriteriaBuilder * @param Search $searchQuery * @param Filter $filterQuery * @param SearchFilter $searchFilter + * @param SearchCriteriaBuilder|null $searchApiCriteriaBuilder */ public function __construct( Builder $searchCriteriaBuilder, Search $searchQuery, Filter $filterQuery, - SearchFilter $searchFilter + SearchFilter $searchFilter, + SearchCriteriaBuilder $searchApiCriteriaBuilder = null ) { $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->searchQuery = $searchQuery; $this->filterQuery = $filterQuery; $this->searchFilter = $searchFilter; + $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ?? + \Magento\Framework\App\ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -70,40 +83,29 @@ public function resolve( array $value = null, array $args = null ) { - $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } if ($args['pageSize'] < 1) { throw new GraphQlInputException(__('pageSize value must be greater than 0.')); } - $searchCriteria->setCurrentPage($args['currentPage']); - $searchCriteria->setPageSize($args['pageSize']); if (!isset($args['search']) && !isset($args['filter'])) { throw new GraphQlInputException( __("'search' or 'filter' input argument is required.") ); - } elseif (isset($args['search'])) { - $layerType = Resolver::CATALOG_LAYER_SEARCH; - $this->searchFilter->add($args['search'], $searchCriteria); - $searchResult = $this->searchQuery->getResult($searchCriteria, $info); - } else { - $layerType = Resolver::CATALOG_LAYER_CATEGORY; - $searchResult = $this->filterQuery->getResult($searchCriteria, $info); - } - //possible division by 0 - if ($searchCriteria->getPageSize()) { - $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); - } else { - $maxPages = 0; } - $currentPage = $searchCriteria->getCurrentPage(); - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { + //get product children fields queried + $productFields = (array)$info->getFieldSelection(1); + $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); + $searchCriteria = $this->searchApiCriteriaBuilder->build($args, $includeAggregations); + $searchResult = $this->searchQuery->getResult($searchCriteria, $info, $args); + + if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { throw new GraphQlInputException( __( 'currentPage value %1 specified is greater than the %2 page(s) available.', - [$currentPage, $maxPages] + [$searchResult->getCurrentPage(), $searchResult->getTotalPages()] ) ); } @@ -112,11 +114,12 @@ public function resolve( 'total_count' => $searchResult->getTotalCount(), 'items' => $searchResult->getProductsSearchResult(), 'page_info' => [ - 'page_size' => $searchCriteria->getPageSize(), - 'current_page' => $currentPage, - 'total_pages' => $maxPages + 'page_size' => $searchResult->getPageSize(), + 'current_page' => $searchResult->getCurrentPage(), + 'total_pages' => $searchResult->getTotalPages() ], - 'layer_type' => $layerType + 'search_result' => $searchResult, + 'layer_type' => isset($args['search']) ? Resolver::CATALOG_LAYER_SEARCH : Resolver::CATALOG_LAYER_CATEGORY, ]; return $data; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php index 3525ccbb6a2d1..b38a2c9bb04d9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php @@ -48,24 +48,57 @@ public function __construct( public function execute(\Iterator $iterator): array { $tree = []; + /** @var CategoryInterface $rootCategory */ + $rootCategory = $iterator->current(); while ($iterator->valid()) { - /** @var CategoryInterface $category */ - $category = $iterator->current(); + /** @var CategoryInterface $currentCategory */ + $currentCategory = $iterator->current(); $iterator->next(); - $pathElements = explode("/", $category->getPath()); - if (empty($tree)) { - $this->startCategoryFetchLevel = count($pathElements) - 1; + if ($this->areParentsActive($currentCategory, $rootCategory, (array)$iterator)) { + $pathElements = explode("/", $currentCategory->getPath()); + if (empty($tree)) { + $this->startCategoryFetchLevel = count($pathElements) - 1; + } + $this->iteratingCategory = $currentCategory; + $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel); + if (empty($tree)) { + $tree = $currentLevelTree; + } + $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree); } - $this->iteratingCategory = $category; - $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel); - if (empty($tree)) { - $tree = $currentLevelTree; - } - $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree); } return $tree; } + /** + * Test that all parents of the current category are active. + * + * Assumes that $categoriesArray are key-pair values and key is the ID of the category and + * all categories in this list are queried as active. + * + * @param CategoryInterface $currentCategory + * @param CategoryInterface $rootCategory + * @param array $categoriesArray + * @return bool + */ + private function areParentsActive( + CategoryInterface $currentCategory, + CategoryInterface $rootCategory, + array $categoriesArray + ): bool { + if ($currentCategory === $rootCategory) { + return true; + } elseif (array_key_exists($currentCategory->getParentId(), $categoriesArray)) { + return $this->areParentsActive( + $categoriesArray[$currentCategory->getParentId()], + $rootCategory, + $categoriesArray + ); + } else { + return false; + } + } + /** * Merge together complex categories trees * diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index e5e0d1aea4285..2076ec6726988 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; @@ -32,7 +33,12 @@ class Product /** * @var CollectionProcessorInterface */ - private $collectionProcessor; + private $collectionPreProcessor; + + /** + * @var CollectionPostProcessor + */ + private $collectionPostProcessor; /** * @var Visibility @@ -44,17 +50,20 @@ class Product * @param ProductSearchResultsInterfaceFactory $searchResultsFactory * @param Visibility $visibility * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionPostProcessor $collectionPostProcessor */ public function __construct( CollectionFactory $collectionFactory, ProductSearchResultsInterfaceFactory $searchResultsFactory, Visibility $visibility, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + CollectionPostProcessor $collectionPostProcessor ) { $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; $this->visibility = $visibility; - $this->collectionProcessor = $collectionProcessor; + $this->collectionPreProcessor = $collectionProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; } /** @@ -75,7 +84,7 @@ public function getList( /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); - $this->collectionProcessor->process($collection, $searchCriteria, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); if (!$isChildSearch) { $visibilityIds = $isSearch @@ -83,18 +92,9 @@ public function getList( : $this->visibility->getVisibleInCatalogIds(); $collection->setVisibility($visibilityIds); } - $collection->load(); - // Methods that perform extra fetches post-load - if (in_array('media_gallery_entries', $attributes)) { - $collection->addMediaGalleryData(); - } - if (in_array('media_gallery', $attributes)) { - $collection->addMediaGalleryData(); - } - if (in_array('options', $attributes)) { - $collection->addOptionsToResult(); - } + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php new file mode 100644 index 0000000000000..fadf22e7643af --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; + +/** + * Processing applied to the collection after load + */ +class CollectionPostProcessor +{ + /** + * Apply processing to loaded product collection + * + * @param Collection $collection + * @param array $attributeNames + * @return Collection + */ + public function process(Collection $collection, array $attributeNames): Collection + { + if (!$collection->isLoaded()) { + $collection->load(); + } + // Methods that perform extra fetches post-load + if (in_array('media_gallery_entries', $attributeNames)) { + $collection->addMediaGalleryData(); + } + if (in_array('media_gallery', $attributeNames)) { + $collection->addMediaGalleryData(); + } + if (in_array('options', $attributeNames)) { + $collection->addOptionsToResult(); + } + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php index f4cefeb3f3638..fef224b12acfc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php @@ -19,7 +19,22 @@ class AttributeProcessor implements CollectionProcessorInterface { /** - * {@inheritdoc} + * Map GraphQl input fields to product attributes + * + * @var array + */ + private $fieldToAttributeMap = []; + + /** + * @param array $fieldToAttributeMap + */ + public function __construct($fieldToAttributeMap = []) + { + $this->fieldToAttributeMap = array_merge($this->fieldToAttributeMap, $fieldToAttributeMap); + } + + /** + * @inheritdoc */ public function process( Collection $collection, @@ -27,9 +42,86 @@ public function process( array $attributeNames ): Collection { foreach ($attributeNames as $name) { - $collection->addAttributeToSelect($name); + $this->addAttribute($collection, $name); } return $collection; } + + /** + * Add attribute to collection select + * + * Add attributes to the collection where graphql fields names don't match attributes names, or if attributes exist + * on a nested level and they need to be loaded. + * + * Format of the attribute can be string or array while array can have different formats. + * Example: [ + * 'price_range' => + * [ + * 'price' => 'price', + * 'price_type' => 'price_type', + * ], + * 'thumbnail' => //complex array where more than one attribute is needed to compute a value + * [ + * 'label' => + * [ + * 'attribute' => 'thumbnail_label', // the actual attribute + * 'fallback_attribute' => 'name', //used as default value in case attribute value is null + * ], + * 'url' => 'thumbnail', + * ] + * ] + * + * @param Collection $collection + * @param string $attribute + */ + private function addAttribute(Collection $collection, string $attribute): void + { + if (isset($this->fieldToAttributeMap[$attribute])) { + $attributeMap = $this->fieldToAttributeMap[$attribute]; + if (is_array($attributeMap)) { + $this->addAttributeAsArray($collection, $attributeMap); + } else { + $collection->addAttributeToSelect($attributeMap); + } + + } else { + $collection->addAttributeToSelect($attribute); + } + } + + /** + * Add an array defined attribute to the collection + * + * @param Collection $collection + * @param array $attributeMap + * @return void + */ + private function addAttributeAsArray(Collection $collection, array $attributeMap): void + { + foreach ($attributeMap as $attribute) { + if (is_array($attribute)) { + $this->addAttributeComplexArrayToCollection($collection, $attribute); + } else { + $collection->addAttributeToSelect($attribute); + } + } + } + + /** + * Add a complex array defined attribute to the collection + * + * @param Collection $collection + * @param array $attribute + * @return void + */ + private function addAttributeComplexArrayToCollection(Collection $collection, array $attribute): void + { + if (isset($attribute['attribute'])) { + $collection->addAttributeToSelect($attribute['attribute']); + } + if (isset($attribute['fallback_attribute'])) { + $collection->addAttributeToSelect($attribute['fallback_attribute']); + } + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php new file mode 100644 index 0000000000000..ff845f4796763 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; + +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; +use Magento\Framework\Api\SearchResultsInterface; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; + +/** + * Product field data provider for product search, used for GraphQL resolver processing. + */ +class ProductSearch +{ + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var ProductSearchResultsInterfaceFactory + */ + private $searchResultsFactory; + + /** + * @var CollectionProcessorInterface + */ + private $collectionPreProcessor; + + /** + * @var CollectionPostProcessor + */ + private $collectionPostProcessor; + + /** + * @var SearchResultApplierFactory; + */ + private $searchResultApplierFactory; + + /** + * @param CollectionFactory $collectionFactory + * @param ProductSearchResultsInterfaceFactory $searchResultsFactory + * @param CollectionProcessorInterface $collectionPreProcessor + * @param CollectionPostProcessor $collectionPostProcessor + * @param SearchResultApplierFactory $searchResultsApplierFactory + */ + public function __construct( + CollectionFactory $collectionFactory, + ProductSearchResultsInterfaceFactory $searchResultsFactory, + CollectionProcessorInterface $collectionPreProcessor, + CollectionPostProcessor $collectionPostProcessor, + SearchResultApplierFactory $searchResultsApplierFactory + ) { + $this->collectionFactory = $collectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->collectionPreProcessor = $collectionPreProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; + $this->searchResultApplierFactory = $searchResultsApplierFactory; + } + + /** + * Get list of product data with full data set. Adds eav attributes to result set from passed in array + * + * @param SearchCriteriaInterface $searchCriteria + * @param SearchResultInterface $searchResult + * @param array $attributes + * @return SearchResultsInterface + */ + public function getList( + SearchCriteriaInterface $searchCriteria, + SearchResultInterface $searchResult, + array $attributes = [] + ): SearchResultsInterface { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + //Join search results + $this->getSearchResultsApplier($searchResult, $collection, $this->getSortOrderArray($searchCriteria))->apply(); + + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); + $collection->load(); + $this->collectionPostProcessor->process($collection, $attributes); + + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setItems($collection->getItems()); + $searchResults->setTotalCount($searchResult->getTotalCount()); + return $searchResults; + } + + /** + * Create searchResultApplier + * + * @param SearchResultInterface $searchResult + * @param Collection $collection + * @param array $orders + * @return SearchResultApplierInterface + */ + private function getSearchResultsApplier( + SearchResultInterface $searchResult, + Collection $collection, + array $orders + ): SearchResultApplierInterface { + return $this->searchResultApplierFactory->create( + [ + 'collection' => $collection, + 'searchResult' => $searchResult, + 'orders' => $orders + ] + ); + } + + /** + * Format sort orders into associative array + * + * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...] + * + * @param SearchCriteriaInterface $searchCriteria + * @return array + */ + private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) + { + $ordersArray = []; + $sortOrders = $searchCriteria->getSortOrders(); + if (is_array($sortOrders)) { + foreach ($sortOrders as $sortOrder) { + $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); + } + } + + return $ordersArray; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php index a547f63b217fe..973b8fbcd6b0f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php @@ -23,13 +23,15 @@ class ProductEntityAttributesForAst implements FieldEntityAttributesInterface private $config; /** + * Additional attributes that are not retrieved by getting fields from ProductInterface + * * @var array */ private $additionalAttributes = ['min_price', 'max_price', 'category_id']; /** * @param ConfigInterface $config - * @param array $additionalAttributes + * @param string[] $additionalAttributes */ public function __construct( ConfigInterface $config, @@ -40,7 +42,12 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * + * Gather all the product entity attributes that can be filtered by search criteria. + * Example format ['attributeNameInGraphQl' => ['type' => 'String'. 'fieldName' => 'attributeNameInSearchCriteria']] + * + * @return array */ public function getEntityAttributes() : array { @@ -55,14 +62,20 @@ public function getEntityAttributes() : array $configElement = $this->config->getConfigElement($interface['interface']); foreach ($configElement->getFields() as $field) { - $fields[$field->getName()] = 'String'; + $fields[$field->getName()] = [ + 'type' => 'String', + 'fieldName' => $field->getName(), + ]; } } - foreach ($this->additionalAttributes as $attribute) { - $fields[$attribute] = 'String'; + foreach ($this->additionalAttributes as $attributeName) { + $fields[$attributeName] = [ + 'type' => 'String', + 'fieldName' => $attributeName, + ]; } - return array_keys($fields); + return $fields; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php new file mode 100644 index 0000000000000..ffa0a3e6848e1 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; + +use GraphQL\Language\AST\SelectionNode; +use Magento\Framework\GraphQl\Query\FieldTranslator; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Extract requested fields from products query + */ +class FieldSelection +{ + /** + * @var FieldTranslator + */ + private $fieldTranslator; + + /** + * @param FieldTranslator $fieldTranslator + */ + public function __construct(FieldTranslator $fieldTranslator) + { + $this->fieldTranslator = $fieldTranslator; + } + + /** + * Get requested fields from products query + * + * @param ResolveInfo $resolveInfo + * @return string[] + */ + public function getProductsFieldSelection(ResolveInfo $resolveInfo): array + { + return $this->getProductFields($resolveInfo); + } + + /** + * Return field names for all requested product fields. + * + * @param ResolveInfo $info + * @return string[] + */ + private function getProductFields(ResolveInfo $info): array + { + $fieldNames = []; + foreach ($info->fieldNodes as $node) { + if ($node->name->value !== 'products' && $node->name->value !== 'variants') { + continue; + } + foreach ($node->selectionSet->selections as $selection) { + if ($selection->name->value !== 'items' && $selection->name->value !== 'product') { + continue; + } + $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); + } + } + if (!empty($fieldNames)) { + $fieldNames = array_merge(...$fieldNames); + } + return $fieldNames; + } + + /** + * Collect field names for each node in selection + * + * @param SelectionNode $selection + * @param array $fieldNames + * @return array + */ + private function collectProductFieldNames(SelectionNode $selection, array $fieldNames = []): array + { + foreach ($selection->selectionSet->selections as $itemSelection) { + if ($itemSelection->kind === 'InlineFragment') { + foreach ($itemSelection->selectionSet->selections as $inlineSelection) { + if ($inlineSelection->kind === 'InlineFragment') { + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); + } + continue; + } + $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); + } + + return $fieldNames; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index 62e2f0c488c6c..cc25af44fdfbe 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -12,7 +12,6 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; -use Magento\Framework\GraphQl\Query\FieldTranslator; /** * Retrieve filtered product data based off given search criteria in a format that GraphQL can interpret. @@ -30,31 +29,31 @@ class Filter private $productDataProvider; /** - * @var FieldTranslator + * @var \Magento\Catalog\Model\Layer\Resolver */ - private $fieldTranslator; + private $layerResolver; /** - * @var \Magento\Catalog\Model\Layer\Resolver + * FieldSelection */ - private $layerResolver; + private $fieldSelection; /** * @param SearchResultFactory $searchResultFactory * @param Product $productDataProvider * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver - * @param FieldTranslator $fieldTranslator + * @param FieldSelection $fieldSelection */ public function __construct( SearchResultFactory $searchResultFactory, Product $productDataProvider, \Magento\Catalog\Model\Layer\Resolver $layerResolver, - FieldTranslator $fieldTranslator + FieldSelection $fieldSelection ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; - $this->fieldTranslator = $fieldTranslator; $this->layerResolver = $layerResolver; + $this->fieldSelection = $fieldSelection; } /** @@ -70,7 +69,7 @@ public function getResult( ResolveInfo $info, bool $isSearch = false ): SearchResult { - $fields = $this->getProductFields($info); + $fields = $this->fieldSelection->getProductsFieldSelection($info); $products = $this->productDataProvider->getList($searchCriteria, $fields, $isSearch); $productArray = []; /** @var \Magento\Catalog\Model\Product $product */ @@ -79,42 +78,11 @@ public function getResult( $productArray[$product->getId()]['model'] = $product; } - return $this->searchResultFactory->create($products->getTotalCount(), $productArray); - } - - /** - * Return field names for all requested product fields. - * - * @param ResolveInfo $info - * @return string[] - */ - private function getProductFields(ResolveInfo $info) : array - { - $fieldNames = []; - foreach ($info->fieldNodes as $node) { - if ($node->name->value !== 'products') { - continue; - } - foreach ($node->selectionSet->selections as $selection) { - if ($selection->name->value !== 'items') { - continue; - } - - foreach ($selection->selectionSet->selections as $itemSelection) { - if ($itemSelection->kind === 'InlineFragment') { - foreach ($itemSelection->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value); - } - } - } - - return $fieldNames; + return $this->searchResultFactory->create( + [ + 'totalCount' => $products->getTotalCount(), + 'productsSearchResult' => $productArray + ] + ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index bc40c664425ff..ef83cc6132ecc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -7,12 +7,13 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ProductSearch; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Api\Search\SearchCriteriaInterface; -use Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\Helper\Filter as FilterHelper; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Search\Api\SearchInterface; +use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory; /** * Full text search for catalog using given search criteria. @@ -25,52 +26,52 @@ class Search private $search; /** - * @var FilterHelper + * @var SearchResultFactory */ - private $filterHelper; + private $searchResultFactory; /** - * @var Filter + * @var \Magento\Search\Model\Search\PageSizeProvider */ - private $filterQuery; + private $pageSizeProvider; /** - * @var SearchResultFactory + * @var SearchCriteriaInterfaceFactory */ - private $searchResultFactory; + private $searchCriteriaFactory; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var FieldSelection */ - private $metadataPool; + private $fieldSelection; /** - * @var \Magento\Search\Model\Search\PageSizeProvider + * @var ProductSearch */ - private $pageSizeProvider; + private $productsProvider; /** * @param SearchInterface $search - * @param FilterHelper $filterHelper - * @param Filter $filterQuery * @param SearchResultFactory $searchResultFactory - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize + * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory + * @param FieldSelection $fieldSelection + * @param ProductSearch $productsProvider */ public function __construct( SearchInterface $search, - FilterHelper $filterHelper, - Filter $filterQuery, SearchResultFactory $searchResultFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, - \Magento\Search\Model\Search\PageSizeProvider $pageSize + \Magento\Search\Model\Search\PageSizeProvider $pageSize, + SearchCriteriaInterfaceFactory $searchCriteriaFactory, + FieldSelection $fieldSelection, + ProductSearch $productsProvider ) { $this->search = $search; - $this->filterHelper = $filterHelper; - $this->filterQuery = $filterQuery; $this->searchResultFactory = $searchResultFactory; - $this->metadataPool = $metadataPool; $this->pageSizeProvider = $pageSize; + $this->searchCriteriaFactory = $searchCriteriaFactory; + $this->fieldSelection = $fieldSelection; + $this->productsProvider = $productsProvider; } /** @@ -81,11 +82,12 @@ public function __construct( * @return SearchResult * @throws \Exception */ - public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $info) : SearchResult - { - $idField = $this->metadataPool->getMetadata( - \Magento\Catalog\Api\Data\ProductInterface::class - )->getIdentifierField(); + public function getResult( + SearchCriteriaInterface $searchCriteria, + ResolveInfo $info + ): SearchResult { + $queryFields = $this->fieldSelection->getProductsFieldSelection($info); + $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround @@ -94,64 +96,39 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); - $ids = []; - $searchIds = []; - foreach ($itemsResults->getItems() as $item) { - $ids[$item->getId()] = null; - $searchIds[] = $item->getId(); - } - - $filter = $this->filterHelper->generate($idField, 'in', $searchIds); - $searchCriteria = $this->filterHelper->remove($searchCriteria, 'search_term'); - $searchCriteria = $this->filterHelper->add($searchCriteria, $filter); - $searchResult = $this->filterQuery->getResult($searchCriteria, $info, true); - - $searchCriteria->setPageSize($realPageSize); - $searchCriteria->setCurrentPage($realCurrentPage); - $paginatedProducts = $this->paginateList($searchResult, $searchCriteria); - - $products = []; - if (!isset($searchCriteria->getSortOrders()[0])) { - foreach ($paginatedProducts as $product) { - if (in_array($product[$idField], $searchIds)) { - $ids[$product[$idField]] = $product; - } - } - $products = array_filter($ids); - } else { - foreach ($paginatedProducts as $product) { - $productId = isset($product['entity_id']) ? $product['entity_id'] : $product[$idField]; - if (in_array($productId, $searchIds)) { - $products[] = $product; - } - } - } + //Create copy of search criteria without conditions (conditions will be applied by joining search result) + $searchCriteriaCopy = $this->searchCriteriaFactory->create() + ->setSortOrders($searchCriteria->getSortOrders()) + ->setPageSize($realPageSize) + ->setCurrentPage($realCurrentPage); - return $this->searchResultFactory->create($searchResult->getTotalCount(), $products); - } + $searchResults = $this->productsProvider->getList($searchCriteriaCopy, $itemsResults, $queryFields); - /** - * Paginate an array of Ids that get pulled back in search based off search criteria and total count. - * - * @param SearchResult $searchResult - * @param SearchCriteriaInterface $searchCriteria - * @return int[] - */ - private function paginateList(SearchResult $searchResult, SearchCriteriaInterface $searchCriteria) : array - { - $length = $searchCriteria->getPageSize(); - // Search starts pages from 0 - $offset = $length * ($searchCriteria->getCurrentPage() - 1); - - if ($searchCriteria->getPageSize()) { - $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); + //possible division by 0 + if ($realPageSize) { + $maxPages = (int)ceil($searchResults->getTotalCount() / $realPageSize); } else { $maxPages = 0; } + $searchCriteria->setPageSize($realPageSize); + $searchCriteria->setCurrentPage($realCurrentPage); - if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { - $offset = (int)$maxPages; + $productArray = []; + /** @var \Magento\Catalog\Model\Product $product */ + foreach ($searchResults->getItems() as $product) { + $productArray[$product->getId()] = $product->getData(); + $productArray[$product->getId()]['model'] = $product; } - return array_slice($searchResult->getProductsSearchResult(), $offset, $length); + + return $this->searchResultFactory->create( + [ + 'totalCount' => $searchResults->getTotalCount(), + 'productsSearchResult' => $productArray, + 'searchAggregation' => $itemsResults->getAggregations(), + 'pageSize' => $realPageSize, + 'currentPage' => $realCurrentPage, + 'totalPages' => $maxPages, + ] + ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php index 6e229bdc38a31..e4a137413b4c5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php @@ -7,31 +7,21 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products; -use Magento\Framework\Api\SearchResultsInterface; +use Magento\Framework\Api\Search\AggregationInterface; /** * Container for a product search holding the item result and the array in the GraphQL-readable product type format. */ class SearchResult { - /** - * @var SearchResultsInterface - */ - private $totalCount; - - /** - * @var array - */ - private $productsSearchResult; + private $data; /** - * @param int $totalCount - * @param array $productsSearchResult + * @param array $data */ - public function __construct(int $totalCount, array $productsSearchResult) + public function __construct(array $data) { - $this->totalCount = $totalCount; - $this->productsSearchResult = $productsSearchResult; + $this->data = $data; } /** @@ -41,7 +31,7 @@ public function __construct(int $totalCount, array $productsSearchResult) */ public function getTotalCount() : int { - return $this->totalCount; + return $this->data['totalCount'] ?? 0; } /** @@ -51,6 +41,46 @@ public function getTotalCount() : int */ public function getProductsSearchResult() : array { - return $this->productsSearchResult; + return $this->data['productsSearchResult'] ?? []; + } + + /** + * Retrieve aggregated search results + * + * @return AggregationInterface|null + */ + public function getSearchAggregation(): ?AggregationInterface + { + return $this->data['searchAggregation'] ?? null; + } + + /** + * Retrieve the page size for the search + * + * @return int + */ + public function getPageSize(): int + { + return $this->data['pageSize'] ?? 0; + } + + /** + * Retrieve the current page for the search + * + * @return int + */ + public function getCurrentPage(): int + { + return $this->data['currentPage'] ?? 0; + } + + /** + * Retrieve total pages for the search + * + * @return int + */ + public function getTotalPages(): int + { + return $this->data['totalPages'] ?? 0; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php index aec9362f47c3a..479e6a3f96235 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php @@ -30,15 +30,15 @@ public function __construct(ObjectManagerInterface $objectManager) /** * Instantiate SearchResult * - * @param int $totalCount - * @param array $productsSearchResult + * @param array $data * @return SearchResult */ - public function create(int $totalCount, array $productsSearchResult) : SearchResult - { + public function create( + array $data + ): SearchResult { return $this->objectManager->create( SearchResult::class, - ['totalCount' => $totalCount, 'productsSearchResult' => $productsSearchResult] + ['data' => $data] ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php new file mode 100644 index 0000000000000..4b3e0a1a58dfd --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Root category tree field resolver, used for GraphQL request processing. + */ +class RootCategoryId implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return (int)$context->getExtensionAttributes()->getStore()->getRootCategoryId(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php b/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php new file mode 100644 index 0000000000000..cfb99ce270c21 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Plugin; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\View\DesignLoader as ViewDesignLoader; +use Magento\Framework\Message\ManagerInterface; +use Magento\Catalog\Block\Product\ImageFactory; + +/** + * Load necessary design files for GraphQL + */ +class DesignLoader +{ + /** + * @var DesignLoader + */ + private $designLoader; + + /** + * @var ManagerInterface + */ + private $messageManager; + + /** + * @param ViewDesignLoader $designLoader + * @param ManagerInterface $messageManager + */ + public function __construct( + ViewDesignLoader $designLoader, + ManagerInterface $messageManager + ) { + $this->designLoader = $designLoader; + $this->messageManager = $messageManager; + } + + /** + * Before create load the design files + * + * @param ImageFactory $subject + * @param Product $product + * @param string $imageId + * @param array|null $attributes + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeCreate( + ImageFactory $subject, + Product $product, + string $imageId, + array $attributes = null + ) { + try { + $this->designLoader->load(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) { + /** @var MessageInterface $message */ + $message = $this->messageManager + ->createMessage(MessageInterface::TYPE_ERROR) + ->setText($e->getMessage()); + $this->messageManager->addUniqueMessages([$message]); + } + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php new file mode 100644 index 0000000000000..992ab50467c72 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -0,0 +1,286 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogGraphQl\Plugin\Search\Request; + +use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\CatalogSearch\Model\Search\RequestGenerator; +use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Search\Request\FilterInterface; +use Magento\Framework\Search\Request\QueryInterface; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection; + +/** + * Add search request configuration to config for give ability filter and search products during GraphQL request + * Add 2 request name with and without aggregation correspondingly: + * - graphql_product_search_with_aggregation + * - graphql_product_search + * + * @see Magento/CatalogGraphQl/etc/search_request.xml + */ +class ConfigReader +{ + /** Bucket name suffix */ + private const BUCKET_SUFFIX = '_bucket'; + + /** + * @var string + */ + private $requestNameWithAggregation = 'graphql_product_search_with_aggregation'; + + /** + * @var string + */ + private $requestName = 'graphql_product_search'; + + /** + * @var GeneratorResolver + */ + private $generatorResolver; + + /** + * @var CollectionFactory + */ + private $productAttributeCollectionFactory; + + /** + * @var array + */ + private $exactMatchAttributes = []; + + /** + * @param GeneratorResolver $generatorResolver + * @param CollectionFactory $productAttributeCollectionFactory + * @param array $exactMatchAttributes + */ + public function __construct( + GeneratorResolver $generatorResolver, + CollectionFactory $productAttributeCollectionFactory, + array $exactMatchAttributes = [] + ) { + $this->generatorResolver = $generatorResolver; + $this->productAttributeCollectionFactory = $productAttributeCollectionFactory; + $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes); + } + + /** + * Merge reader's value with generated + * + * @param \Magento\Framework\Config\ReaderInterface $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterRead( + \Magento\Framework\Config\ReaderInterface $subject, + array $result + ) { + $searchRequestNameWithAggregation = $this->generateRequest(); + $searchRequest = $searchRequestNameWithAggregation; + $searchRequest['queries'][$this->requestName] = $searchRequest['queries'][$this->requestNameWithAggregation]; + unset($searchRequest['queries'][$this->requestNameWithAggregation], $searchRequest['aggregations']); + + return array_merge_recursive( + $result, + [ + $this->requestNameWithAggregation => $searchRequestNameWithAggregation, + $this->requestName => $searchRequest, + ] + ); + } + + /** + * Retrieve searchable attributes + * + * @return Attribute[] + */ + private function getSearchableAttributes(): array + { + $attributes = []; + /** @var Collection $productAttributes */ + $productAttributes = $this->productAttributeCollectionFactory->create(); + $productAttributes->addFieldToFilter( + ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'], + [1, 1, [1, 2], 1] + ); + + /** @var Attribute $attribute */ + foreach ($productAttributes->getItems() as $attribute) { + $attributes[$attribute->getAttributeCode()] = $attribute; + } + + return $attributes; + } + + /** + * Generate search request for search products via GraphQL + * + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function generateRequest() + { + $request = []; + foreach ($this->getSearchableAttributes() as $attribute) { + if (\in_array($attribute->getAttributeCode(), ['price', 'visibility', 'category_ids'])) { + //some fields have special semantics + continue; + } + $queryName = $attribute->getAttributeCode() . '_query'; + $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX; + $request['queries'][$this->requestNameWithAggregation]['queryReference'][] = [ + 'clause' => 'must', + 'ref' => $queryName, + ]; + + switch ($attribute->getBackendType()) { + case 'static': + case 'text': + case 'varchar': + if ($this->isExactMatchAttribute($attribute)) { + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); + } else { + $request['queries'][$queryName] = $this->generateMatchQuery($queryName, $attribute); + } + break; + case 'decimal': + case 'datetime': + case 'date': + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateRangeFilter($filterName, $attribute); + break; + default: + $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName); + $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute); + } + $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + + if ($attribute->getData(EavAttributeInterface::IS_FILTERABLE)) { + $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; + $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); + } + + $this->addSearchAttributeToFullTextSearch($attribute, $request); + } + + return $request; + } + + /** + * Add attribute with specified boost to "search" query used in full text search + * + * @param Attribute $attribute + * @param array $request + * @return void + */ + private function addSearchAttributeToFullTextSearch(Attribute $attribute, &$request): void + { + // Match search by custom price attribute isn't supported + if ($attribute->getFrontendInput() !== 'price') { + $request['queries']['search']['match'][] = [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ]; + } + } + + /** + * Return array representation of range filter + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateRangeFilter(string $filterName, Attribute $attribute) + { + return [ + 'field' => $attribute->getAttributeCode(), + 'name' => $filterName, + 'type' => FilterInterface::TYPE_RANGE, + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * Return array representation of term filter + * + * @param string $filterName + * @param Attribute $attribute + * @return array + */ + private function generateTermFilter(string $filterName, Attribute $attribute) + { + return [ + 'type' => FilterInterface::TYPE_TERM, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'value' => '$' . $attribute->getAttributeCode() . '$', + ]; + } + + /** + * Return array representation of query based on filter + * + * @param string $queryName + * @param string $filterName + * @return array + */ + private function generateFilterQuery(string $queryName, string $filterName) + { + return [ + 'name' => $queryName, + 'type' => QueryInterface::TYPE_FILTER, + 'filterReference' => [ + [ + 'ref' => $filterName, + ], + ], + ]; + } + + /** + * Return array representation of match query + * + * @param string $queryName + * @param Attribute $attribute + * @return array + */ + private function generateMatchQuery(string $queryName, Attribute $attribute) + { + return [ + 'name' => $queryName, + 'type' => 'matchQuery', + 'value' => '$' . $attribute->getAttributeCode() . '$', + 'match' => [ + [ + 'field' => $attribute->getAttributeCode(), + 'boost' => $attribute->getSearchWeight() ?: 1, + ], + ], + ]; + } + + /** + * Check if attribute's filter should use exact match + * + * @param Attribute $attribute + * @return bool + */ + private function isExactMatchAttribute(Attribute $attribute) + { + if (in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) { + return true; + } + if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) { + return true; + } + + return false; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php new file mode 100644 index 0000000000000..5ebb48f761c06 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Product\Price; + +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; +use PHPUnit\Framework\TestCase; + +class DiscountTest extends TestCase +{ + /** + * @var Discount + */ + private $discount; + + protected function setUp() + { + $this->discount = new Discount(); + } + + /** + * @dataProvider priceDataProvider + * @param $regularPrice + * @param $finalPrice + * @param $expectedAmountOff + * @param $expectedPercentOff + */ + public function testGetPriceDiscount($regularPrice, $finalPrice, $expectedAmountOff, $expectedPercentOff) + { + $discountResult = $this->discount->getDiscountByDifference($regularPrice, $finalPrice); + + $this->assertEquals($expectedAmountOff, $discountResult['amount_off']); + $this->assertEquals($expectedPercentOff, $discountResult['percent_off']); + } + + /** + * Price data provider + * + * [regularPrice, finalPrice, expectedAmountOff, expectedPercentOff] + * + * @return array + */ + public function priceDataProvider() + { + return [ + [100, 50, 50, 50], + [.1, .05, .05, 50], + [12.50, 10, 2.5, 20], + [99.99, 84.99, 15.0, 15], + [9999999999.01, 8999999999.11, 999999999.9, 10], + [0, 0, 0, 0], + [0, 10, 0, 0], + [9.95, 9.95, 0, 0], + [21.05, 0, 21.05, 100] + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index 13fcbe9a7d357..1582f29c25951 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -10,6 +10,7 @@ "magento/module-search": "*", "magento/module-store": "*", "magento/module-eav-graph-ql": "*", + "magento/module-catalog-search": "*", "magento/framework": "*" }, "suggest": { diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index a5006355ed265..1fe62fc442ecf 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -19,6 +19,8 @@ <argument name="readers" xsi:type="array"> <item name="productDynamicAttributeReader" xsi:type="object">Magento\CatalogGraphQl\Model\Config\AttributeReader</item> <item name="categoryDynamicAttributeReader" xsi:type="object">Magento\CatalogGraphQl\Model\Config\CategoryAttributeReader</item> + <item name="productSortDynamicAttributeReader" xsi:type="object">Magento\CatalogGraphQl\Model\Config\SortAttributeReader</item> + <item name="productFilterDynamicAttributeReader" xsi:type="object">Magento\CatalogGraphQl\Model\Config\FilterAttributeReader</item> </argument> </arguments> </virtualType> @@ -55,4 +57,18 @@ <argument name="searchCriteriaApplier" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor</argument> </arguments> </type> + + <type name="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader"> + <arguments> + <argument name="exactMatchAttributes" xsi:type="array"> + <item name="sku" xsi:type="string">sku</item> + </argument> + </arguments> + </type> + + <type name="Magento\Framework\Search\Request\Config\FilesystemReader"> + <plugin name="productAttributesDynamicFields" type="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader" /> + </type> + + <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 2292004f3cf01..066a7b38d8967 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -28,6 +28,13 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\AggregationOptionTypeResolverComposite"> + <arguments> + <argument name="typeResolvers" xsi:type="array"> + <item name="aggregation_option" xsi:type="object">Magento\CatalogGraphQl\Model\AggregationOptionTypeResolver</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\GraphQl\Schema\Type\Entity\DefaultMapper"> <arguments> <argument name="map" xsi:type="array"> @@ -48,6 +55,12 @@ <item name="radio" xsi:type="string">CustomizableRadioOption</item> <item name="checkbox" xsi:type="string">CustomizableCheckboxOption</item> </item> + <item name="sort_attributes" xsi:type="array"> + <item name="product_sort_attributes" xsi:type="string">ProductAttributeSortInput</item> + </item> + <item name="filter_attributes" xsi:type="array"> + <item name="product_filter_attributes" xsi:type="string">ProductAttributeFilterInput</item> + </item> </argument> </arguments> </type> @@ -95,4 +108,72 @@ </argument> </arguments> </type> + + <type name="Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder"> + <arguments> + <argument name="builders" xsi:type="array"> + <item name="price_bucket" xsi:type="object">Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Price</item> + <item name="category_bucket" xsi:type="object">Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Category</item> + <item name="attribute_bucket" xsi:type="object">Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Attribute</item> + </argument> + </arguments> + </type> + + + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\AttributeProcessor"> + <arguments> + <argument name="fieldToAttributeMap" xsi:type="array"> + <item name="price_range" xsi:type="array"> + <item name="price" xsi:type="string">price</item> + </item> + <item name="thumbnail" xsi:type="array"> + <item name="label" xsi:type="array"> + <item name="attribute" xsi:type="string">thumbnail_label</item> + <item name="fallback_attribute" xsi:type="string">name</item> + </item> + <item name="url" xsi:type="string">thumbnail</item> + </item> + <item name="small_image" xsi:type="array"> + <item name="label" xsi:type="array"> + <item name="attribute" xsi:type="string">small_image_label</item> + <item name="fallback_attribute" xsi:type="string">name</item> + </item> + <item name="url" xsi:type="string">small_image</item> + </item> + <item name="image" xsi:type="array"> + <item name="label" xsi:type="array"> + <item name="attribute" xsi:type="string">image_label</item> + <item name="fallback_attribute" xsi:type="string">name</item> + </item> + <item name="url" xsi:type="string">image</item> + </item> + <item name="media_gallery" xsi:type="array"> + <item name="label" xsi:type="array"> + <item name="attribute" xsi:type="string">image_label</item> + <item name="fallback_attribute" xsi:type="string">name</item> + </item> + </item> + </argument> + </arguments> + </type> + + <type name="Magento\Catalog\Block\Product\ImageFactory"> + <plugin name="designLoader" type="Magento\CatalogGraphQl\Plugin\DesignLoader" /> + </type> + + <type name="Magento\CatalogGraphQl\Model\Config\CategoryAttributeReader"> + <arguments> + <argument name="categoryAttributeResolvers" xsi:type="array"> + <item name="image" xsi:type="string">Magento\CatalogGraphQl\Model\Resolver\Category\Image</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index ea56faf94408e..f70a32a1b549e 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -4,33 +4,36 @@ type Query { products ( search: String @doc(description: "Performs a full-text search using the specified key words."), - filter: ProductFilterInput @doc(description: "Identifies which product attributes to search for and return."), + filter: ProductAttributeFilterInput @doc(description: "Identifies which product attributes to search for and return."), pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( - id: Int @doc(description: "Id of the category.") + id: Int @doc(description: "Id of the category.") ): CategoryTree - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + categoryList( + filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") + ): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") } -type Price @doc(description: "The Price object defines the price of a product as well as any tax-related adjustments.") { - amount: Money @doc(description: "The price of a product plus a three-letter currency code.") - adjustments: [PriceAdjustment] @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.") +type Price @doc(description: "Price is deprecated, replaced by ProductPrice. The Price object defines the price of a product as well as any tax-related adjustments.") { + amount: Money @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "The price of a product plus a three-letter currency code.") + adjustments: [PriceAdjustment] @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.") } -type PriceAdjustment @doc(description: "The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") { +type PriceAdjustment @doc(description: "PriceAdjustment is deprecated. Taxes will be included or excluded in the price. The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") { amount: Money @doc(description: "The amount of the price adjustment and its currency code.") - code: PriceAdjustmentCodesEnum @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.") - description: PriceAdjustmentDescriptionEnum @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.") + code: PriceAdjustmentCodesEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.") + description: PriceAdjustmentDescriptionEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.") } -enum PriceAdjustmentCodesEnum @doc(description: "Note: This enumeration contains values defined in modules other than the Catalog module.") { +enum PriceAdjustmentCodesEnum @doc(description: "PriceAdjustment.code is deprecated. This enumeration contains values defined in modules other than the Catalog module.") { } -enum PriceAdjustmentDescriptionEnum @doc(description: "This enumeration states whether a price adjustment is included or excluded.") { +enum PriceAdjustmentDescriptionEnum @doc(description: "PriceAdjustmentDescriptionEnum is deprecated. This enumeration states whether a price adjustment is included or excluded.") { INCLUDED EXCLUDED } @@ -41,10 +44,26 @@ enum PriceTypeEnum @doc(description: "This enumeration the price type.") { DYNAMIC } -type ProductPrices @doc(description: "The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { - minimalPrice: Price @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") - maximalPrice: Price @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") - regularPrice: Price @doc(description: "The base price of a product.") +type ProductPrices @doc(description: "ProductPrices is deprecated, replaced by PriceRange. The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { + minimalPrice: Price @deprecated(reason: "Use PriceRange.minimum_price.") @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") + maximalPrice: Price @deprecated(reason: "Use PriceRange.maximum_price.") @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") + regularPrice: Price @deprecated(reason: "Use regular_price from PriceRange.minimum_price or PriceRange.maximum_price.") @doc(description: "The base price of a product.") +} + +type PriceRange @doc(description: "Price range for a product. If the product has a single price, the minimum and maximum price will be the same."){ + minimum_price: ProductPrice! @doc(description: "The lowest possible price for the product.") + maximum_price: ProductPrice @doc(description: "The highest possible price for the product.") +} + +type ProductPrice @doc(description: "Represents a product price.") { + regular_price: Money! @doc(description: "The regular price of the product.") + final_price: Money! @doc(description: "The final price of the product after discounts applied.") + discount: ProductDiscount @doc(description: "The price discount. Represents the difference between the regular and final price.") +} + +type ProductDiscount @doc(description: "A discount applied to a product price.") { + percent_off: Float @doc(description: "The discount expressed a percentage.") + amount_off: Float @doc(description: "The actual value of the discount.") } type ProductLinks implements ProductLinksInterface @doc(description: "ProductLinks is an implementation of ProductLinksInterface.") { @@ -58,14 +77,6 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M position: Int @doc(description: "The position within the list of product links.") } -type ProductTierPrices @doc(description: "The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") { - customer_group_id: String @doc(description: "The ID of the customer group.") - qty: Float @doc(description: "The number of items that must be purchased to qualify for tier pricing.") - value: Float @doc(description: "The price of the fixed price item.") - percentage_value: Float @doc(description: "The percentage discount of the item.") - website_id: Float @doc(description: "The ID assigned to the website.") -} - interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") name: String @doc(description: "The product name. Customers use this name to identify the product.") @@ -84,21 +95,21 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") - tier_price: Float @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") + tier_price: Float @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.") created_at: String @doc(description: "Timestamp indicating when the product was created.") updated_at: String @doc(description: "Timestamp indicating when the product was updated.") country_of_manufacture: String @doc(description: "The product's country of origin.") type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.") websites: [Website] @doc(description: "An array of websites in which the product is available.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") - product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductLinks") + product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\BatchProductLinks") media_gallery_entries: [MediaGalleryEntry] @deprecated(reason: "Use product's `media_gallery` instead") @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") - tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\TierPrices") - price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") + price: ProductPrices @deprecated(reason: "Use price_range for product price information.") @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") + price_range: PriceRange! @doc(description: "A PriceRange object, indicating the range of prices for the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\PriceRange") gift_message_available: String @doc(description: "Indicates whether a gift message is available.") manufacturer: Int @doc(description: "A number representing the product's manufacturer.") categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") - canonical_url: String @doc(description: "Canonical URL.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") + canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") media_gallery: [MediaGalleryInterface] @doc(description: "An array of Media Gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery") } @@ -187,7 +198,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { url: String @doc(description: "The URL of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Url") - label: String @doc(description: "The label of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Label") + label: String @doc(description: "The label of the product image or video.") } type ProductImage implements MediaGalleryInterface @doc(description: "Product image information. Contains the image URL and label.") { @@ -212,6 +223,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model path_in_store: String @doc(description: "Category path in store.") url_key: String @doc(description: "The url key assigned to the category.") url_path: String @doc(description: "The url path assigned to the category.") + canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Categories' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CanonicalUrl") position: Int @doc(description: "The position of the category relative to other categories at the same level in tree.") level: Int @doc(description: "Indicates the depth of the category within the tree.") created_at: String @doc(description: "Timestamp indicating when the category was created.") @@ -221,7 +233,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model products( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } @@ -231,6 +243,7 @@ type Breadcrumb @doc(description: "Breadcrumb item."){ category_name: String @doc(description: "Category name.") category_level: Int @doc(description: "Category level.") category_url_key: String @doc(description: "Category URL key.") + category_url_path: String @doc(description: "Category URL path.") } type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option.") { @@ -270,7 +283,8 @@ type Products @doc(description: "The Products object is the top-level object ret items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria.") page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") total_count: Int @doc(description: "The number of products returned.") - filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") + filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead") + aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } @@ -280,7 +294,18 @@ type CategoryProducts @doc(description: "The category products object returned i total_count: Int @doc(description: "The number of products returned.") } -input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { +input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { + category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") +} + +input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") +{ + ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.") + url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category") + name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.") +} + +input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -333,7 +358,7 @@ type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalle video_metadata: String @doc(description: "Optional data about the video.") } -input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { +input ProductSortInput @doc(description: "ProductSortInput is deprecated, use @ProductAttributeSortInput instead. ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -367,6 +392,12 @@ input ProductSortInput @doc(description: "ProductSortInput specifies the attribu gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.") } +input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option") +{ + relevance: SortEnum @doc(description: "Sort by the search relevance score (default).") + position: SortEnum @doc(description: "Sort by the position assigned to each product.") +} + type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { id: Int @doc(description: "The identifier assigned to the object.") media_type: String @doc(description: "image or video.") @@ -380,22 +411,39 @@ type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characterist } type LayerFilter { - name: String @doc(description: "Layered navigation filter name.") - request_var: String @doc(description: "Request variable name for filter query.") - filter_items_count: Int @doc(description: "Count of filter items in filter group.") - filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") + name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead.") + request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead.") + filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead.") + filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead.") } interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") { - label: String @doc(description: "Filter label.") - value_string: String @doc(description: "Value for filter request variable to be used in query.") - items_count: Int @doc(description: "Count of items by filter.") + label: String @doc(description: "Filter label.") @deprecated(reason: "Use AggregationOption.label instead.") + value_string: String @doc(description: "Value for filter request variable to be used in query.") @deprecated(reason: "Use AggregationOption.value instead.") + items_count: Int @doc(description: "Count of items by filter.") @deprecated(reason: "Use AggregationOption.count instead.") } type LayerFilterItem implements LayerFilterItemInterface { } +type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { + count: Int @doc(description: "The number of options in the aggregation group.") + label: String @doc(description: "The aggregation display name.") + attribute_code: String! @doc(description: "Attribute code of the aggregation group.") + options: [AggregationOption] @doc(description: "Array of options for the aggregation.") +} + +interface AggregationOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\AggregationOptionTypeResolverComposite") { + count: Int @doc(description: "The number of items that match the aggregation option.") + label: String @doc(description: "Aggregation option display label.") + value: String! @doc(description: "The internal ID that represents the value of the option.") +} + +type AggregationOption implements AggregationOptionInterface { + +} + type SortField { value: String @doc(description: "Attribute code of sort field.") label: String @doc(description: "Label of sort field.") @@ -416,6 +464,7 @@ type StoreConfig @doc(description: "The type contains information about a store grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.") list_per_page : Int @doc(description: "Products per Page on List Default Value.") catalog_default_sort_by : String @doc(description: "Default Sort By.") + root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") } type ProductVideo @doc(description: "Contains information about a product video.") implements MediaGalleryInterface { diff --git a/app/code/Magento/CatalogGraphQl/etc/search_request.xml b/app/code/Magento/CatalogGraphQl/etc/search_request.xml new file mode 100644 index 0000000000000..ab1eea9eb6fda --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/etc/search_request.xml @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<requests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Search/etc/search_request.xsd"> + <!-- Request schema for product search including aggregation --> + <request query="graphql_product_search_with_aggregation" index="catalogsearch_fulltext"> + <dimensions> + <dimension name="scope" value="default"/> + </dimensions> + <queries> + <query xsi:type="boolQuery" name="graphql_product_search_with_aggregation" boost="1"> + <queryReference clause="should" ref="search" /> + <queryReference clause="must" ref="category"/> + <queryReference clause="must" ref="price"/> + <queryReference clause="must" ref="visibility"/> + </query> + <query xsi:type="matchQuery" value="$search_term$" name="search"> + <match field="sku"/> + <match field="*"/> + </query> + <query name="category" xsi:type="filteredQuery"> + <filterReference clause="must" ref="category_filter"/> + </query> + <query name="price" xsi:type="filteredQuery"> + <filterReference clause="must" ref="price_filter"/> + </query> + <query name="visibility" xsi:type="filteredQuery"> + <filterReference clause="must" ref="visibility_filter"/> + </query> + </queries> + <filters> + <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_id$"/> + <filter xsi:type="rangeFilter" name="price_filter" field="price" from="$price.from$" to="$price.to$"/> + <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/> + </filters> + <aggregations> + <bucket name="price_bucket" field="price" xsi:type="dynamicBucket" method="$price_dynamic_algorithm$"> + <metrics> + <metric type="count"/> + </metrics> + </bucket> + <bucket name="category_bucket" field="category_ids" xsi:type="termBucket"> + <metrics> + <metric type="count"/> + </metrics> + </bucket> + </aggregations> + <from>0</from> + <size>10000</size> + </request> + <!-- Request schema for product search excluding aggregation --> + <request query="graphql_product_search" index="catalogsearch_fulltext"> + <dimensions> + <dimension name="scope" value="default"/> + </dimensions> + <queries> + <query xsi:type="boolQuery" name="graphql_product_search" boost="1"> + <queryReference clause="should" ref="search" /> + <queryReference clause="must" ref="category"/> + <queryReference clause="must" ref="price"/> + <queryReference clause="must" ref="visibility"/> + </query> + <query xsi:type="matchQuery" value="$search_term$" name="search"> + <match field="sku"/> + <match field="*"/> + </query> + <query name="category" xsi:type="filteredQuery"> + <filterReference clause="must" ref="category_filter"/> + </query> + <query name="price" xsi:type="filteredQuery"> + <filterReference clause="must" ref="price_filter"/> + </query> + <query name="visibility" xsi:type="filteredQuery"> + <filterReference clause="must" ref="visibility_filter"/> + </query> + </queries> + <filters> + <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_id$"/> + <filter xsi:type="rangeFilter" name="price_filter" field="price" from="$price.from$" to="$price.to$"/> + <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/> + </filters> + <from>0</from> + <size>10000</size> + </request> +</requests> diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 428c61c7fec0f..5baa4b4274be5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -484,7 +484,9 @@ protected function initTypeModels() } if ($model->isSuitable()) { $this->_productTypeModels[$productTypeName] = $model; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_disabledAttrs = array_merge($this->_disabledAttrs, $model->getDisabledAttrs()); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_indexValueAttributes = array_merge( $this->_indexValueAttributes, $model->getIndexValueAttributes() @@ -526,7 +528,7 @@ protected function getMediaGallery(array $productIds) if (empty($productIds)) { return []; } - + $productEntityJoinField = $this->getProductEntityLinkField(); $select = $this->_connection->select()->from( @@ -710,6 +712,21 @@ public function _getHeaderColumns() return $this->_customHeadersMapping($this->rowCustomizer->addHeaderColumns($this->_headerColumns)); } + /** + * Return non-system attributes + + * @return array + */ + private function getNonSystemAttributes(): array + { + $attrKeys = []; + foreach ($this->filterAttributeCollection($this->getAttributeCollection()) as $attribute) { + $attrKeys[] = $attribute->getAttributeCode(); + } + + return array_diff($this->_getExportMainAttrCodes(), $this->_customHeadersMapping($attrKeys)); + } + /** * Set headers columns * @@ -722,6 +739,18 @@ public function _getHeaderColumns() */ protected function setHeaderColumns($customOptionsData, $stockItemRows) { + $exportAttributes = ( + array_key_exists("skip_attr", $this->_parameters) && count($this->_parameters["skip_attr"]) + ) ? + array_intersect( + $this->_getExportMainAttrCodes(), + array_merge( + $this->_customHeadersMapping($this->_getExportAttrCodes()), + $this->getNonSystemAttributes() + ) + ) : + $this->_getExportMainAttrCodes(); + if (!$this->_headerColumns) { $this->_headerColumns = array_merge( [ @@ -732,7 +761,7 @@ protected function setHeaderColumns($customOptionsData, $stockItemRows) self::COL_CATEGORY, self::COL_PRODUCT_WEBSITES, ], - $this->_getExportMainAttrCodes(), + $exportAttributes, [self::COL_ADDITIONAL_ATTRIBUTES], reset($stockItemRows) ? array_keys(end($stockItemRows)) : [], [ @@ -923,6 +952,7 @@ protected function getExportData() foreach ($rawData as $productId => $productData) { foreach ($productData as $storeId => $dataRow) { if ($storeId == Store::DEFAULT_STORE_ID && isset($stockItemRows[$productId])) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $dataRow = array_merge($dataRow, $stockItemRows[$productId]); } $this->appendMultirowData($dataRow, $multirawData); @@ -1330,7 +1360,7 @@ private function appendMultirowData(&$dataRow, $multiRawData) $dataRow[self::COL_SKU] = $sku; $dataRow[self::COL_ATTR_SET] = $attributeSet; $dataRow[self::COL_TYPE] = $type; - + return $dataRow; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 4ff995c2a872c..8f70ea88f4ba7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -141,7 +141,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity const COL_PRODUCT_WEBSITES = '_product_websites'; /** - * Media gallery attribute code. + * Attribute code for media gallery. */ const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery'; @@ -151,12 +151,12 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity const COL_MEDIA_IMAGE = '_media_image'; /** - * Inventory use config. + * Inventory use config label. */ const INVENTORY_USE_CONFIG = 'Use Config'; /** - * Inventory use config prefix. + * Prefix for inventory use config. */ const INVENTORY_USE_CONFIG_PREFIX = 'use_config_'; @@ -302,6 +302,9 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', + // Can't add new translated strings in patch release + 'invalidLayoutUpdate' => 'Invalid format.', + 'insufficientPermissions' => 'Invalid format.', ]; //@codingStandardsIgnoreEnd @@ -1195,7 +1198,7 @@ protected function _initTypeModels() // phpcs:disable Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge $this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping()); $this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes()); - // phpcs:enable + // phpcs:enable } $this->_initErrorTemplates(); // remove doubles @@ -1511,7 +1514,7 @@ public function getImagesFromRow(array $rowData) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @throws LocalizedException - * phpcs:disable Generic.Metrics.NestingLevel + * phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ protected function _saveProducts() { @@ -1886,6 +1889,7 @@ protected function _saveProducts() return $this; } + //phpcs:enable Generic.Metrics.NestingLevel /** * Prepare array with image states (visible or hidden from product page) @@ -2031,9 +2035,9 @@ protected function _saveProductTierPrices(array $tierPriceData) protected function _getUploader() { if ($this->_fileUploader === null) { - $this->_fileUploader = $this->_uploaderFactory->create(); + $fileUploader = $this->_uploaderFactory->create(); - $this->_fileUploader->init(); + $fileUploader->init(); $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; @@ -2044,7 +2048,7 @@ protected function _getUploader() $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import'); } - if (!$this->_fileUploader->setTmpDir($tmpPath)) { + if (!$fileUploader->setTmpDir($tmpPath)) { throw new LocalizedException( __('File directory \'%1\' is not readable.', $tmpPath) ); @@ -2053,11 +2057,13 @@ protected function _getUploader() $destinationPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath($destinationDir); $this->_mediaDirectory->create($destinationPath); - if (!$this->_fileUploader->setDestDir($destinationPath)) { + if (!$fileUploader->setDestDir($destinationPath)) { throw new LocalizedException( __('File directory \'%1\' is not writable.', $destinationPath) ); } + + $this->_fileUploader = $fileUploader; } return $this->_fileUploader; } @@ -2736,8 +2742,6 @@ protected function _saveValidatedBunches() try { $rowData = $source->current(); } catch (\InvalidArgumentException $e) { - $this->addRowError($e->getMessage(), $this->_processedRowsCount); - $this->_processedRowsCount++; $source->next(); continue; } @@ -3058,6 +3062,8 @@ private function getValidationErrorLevel($sku): string * @param int $nextLinkId * @param array $positionAttrId * @return void + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function processLinkBunches( array $bunch, @@ -3068,6 +3074,7 @@ private function processLinkBunches( $productIds = []; $linkRows = []; $positionRows = []; + $linksToDelete = []; $bunch = array_filter($bunch, [$this, 'isRowAllowedToImport'], ARRAY_FILTER_USE_BOTH); foreach ($bunch as $rowData) { @@ -3084,10 +3091,15 @@ function ($linkName) use ($rowData) { ); foreach ($linkNameToId as $linkName => $linkId) { $linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']); + //process empty value + if (!empty($linkSkus[0]) && $linkSkus[0] === $this->getEmptyAttributeValueConstant()) { + $linksToDelete[$linkId][] = $productId; + continue; + } + $linkPositions = !empty($rowData[$linkName . 'position']) ? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position']) : []; - $linkSkus = array_filter( $linkSkus, function ($linkedSku) use ($sku) { @@ -3096,6 +3108,7 @@ function ($linkedSku) use ($sku) { && strcasecmp($linkedSku, $sku) !== 0; } ); + foreach ($linkSkus as $linkedKey => $linkedSku) { $linkedId = $this->getProductLinkedId($linkedSku); if ($linkedId == null) { @@ -3127,9 +3140,34 @@ function ($linkedSku) use ($sku) { } } } + $this->deleteProductsLinks($resource, $linksToDelete); $this->saveLinksData($resource, $productIds, $linkRows, $positionRows); } + /** + * Delete links + * + * @param Link $resource + * @param array $linksToDelete + * @return void + * @throws LocalizedException + */ + private function deleteProductsLinks(Link $resource, array $linksToDelete) + { + if (!empty($linksToDelete) && Import::BEHAVIOR_APPEND === $this->getBehavior()) { + foreach ($linksToDelete as $linkTypeId => $productIds) { + if (!empty($productIds)) { + $whereLinkId = $this->_connection->quoteInto('link_type_id', $linkTypeId); + $whereProductId = $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds)); + $this->_connection->delete( + $resource->getMainTable(), + $whereLinkId . ' AND ' . $whereProductId + ); + } + } + } + } + /** * Fetches Product Links * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index 4d8088a235402..4c716421b7ae6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -1695,7 +1695,7 @@ protected function _parseRequiredData(array $rowData) $this->_rowIsMain = false; } - return [$this->_rowProductId, $this->_rowStoreId, $this->_rowType, $this->_rowIsMain]; + return true; } /** @@ -2086,7 +2086,9 @@ protected function _parseCustomOptions($rowData) } } } - $options[$name][$k]['_custom_option_store'] = $rowData[Product::COL_STORE_VIEW_CODE]; + if (isset($rowData[Product::COL_STORE_VIEW_CODE])) { + $options[$name][$k][self::COLUMN_STORE] = $rowData[Product::COL_STORE_VIEW_CODE]; + } $k++; } $rowData['custom_options'] = $options; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 3b6caef66ce6c..d87c3d8477556 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -13,6 +13,7 @@ /** * Import entity abstract product type model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @SuppressWarnings(PHPMD.TooManyFields) @@ -543,7 +544,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe } else { $resultAttrs[$attrCode] = $rowData[$attrCode]; } - } elseif (array_key_exists($attrCode, $rowData) && empty($rowData['_store'])) { + } elseif (array_key_exists($attrCode, $rowData)) { $resultAttrs[$attrCode] = $rowData[$attrCode]; } elseif ($withDefaultValue && null !== $attrParams['default_value'] && empty($rowData['_store'])) { $resultAttrs[$attrCode] = $attrParams['default_value']; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php new file mode 100644 index 0000000000000..99919628518c6 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import\Product\Validator; + +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; + +/** + * Validates layout and custom layout update fields + */ +class LayoutUpdate extends AbstractImportValidator +{ + private const ERROR_INVALID_LAYOUT_UPDATE = 'invalidLayoutUpdate'; + + /** + * @var ValidatorFactory + */ + private $layoutValidatorFactory; + + /** + * @var ValidationStateInterface + */ + private $validationState; + + /** + * @param ValidatorFactory $layoutValidatorFactory + * @param ValidationStateInterface $validationState + */ + public function __construct( + ValidatorFactory $layoutValidatorFactory, + ValidationStateInterface $validationState + ) { + $this->layoutValidatorFactory = $layoutValidatorFactory; + $this->validationState = $validationState; + } + + /** + * @inheritdoc + */ + public function isValid($value): bool + { + if (!empty($value['custom_layout_update']) && !$this->validateXml($value['custom_layout_update'])) { + $this->_addMessages( + [ + $this->context->retrieveMessageTemplate(self::ERROR_INVALID_LAYOUT_UPDATE) + ] + ); + return false; + } + + return true; + } + + /** + * Validate XML layout update + * + * @param string $xml + * @return bool + */ + private function validateXml(string $xml): bool + { + /** @var $layoutXmlValidator \Magento\Framework\View\Model\Layout\Update\Validator */ + $layoutXmlValidator = $this->layoutValidatorFactory->create( + [ + 'validationState' => $this->validationState, + ] + ); + + try { + if (!$layoutXmlValidator->isValid($xml)) { + return false; + } + } catch (\Exception $e) { + return false; + } + + return true; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php new file mode 100644 index 0000000000000..50d38cedfb754 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import\Product\Validator; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; + +/** + * Validator to assert that the current user is allowed to make design updates if a layout is provided in the import + */ +class LayoutUpdatePermissions extends AbstractImportValidator +{ + private const ERROR_INSUFFICIENT_PERMISSIONS = 'insufficientPermissions'; + + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var array + */ + private $allowedUserTypes = [ + UserContextInterface::USER_TYPE_ADMIN, + UserContextInterface::USER_TYPE_INTEGRATION + ]; + + /** + * @param UserContextInterface $userContext + * @param AuthorizationInterface $authorization + */ + public function __construct( + UserContextInterface $userContext, + AuthorizationInterface $authorization + ) { + $this->userContext = $userContext; + $this->authorization = $authorization; + } + + /** + * Validate that the current user is allowed to make design updates + * + * @param array $data + * @return boolean + */ + public function isValid($data): bool + { + if (empty($data['custom_layout_update'])) { + return true; + } + + $userType = $this->userContext->getUserType(); + $isValid = in_array($userType, $this->allowedUserTypes) + && $this->authorization->isAllowed('Magento_Catalog::edit_product_design'); + + if (!$isValid) { + $this->_addMessages( + [ + $this->context->retrieveMessageTemplate(self::ERROR_INSUFFICIENT_PERMISSIONS), + ] + ); + } + + return $isValid; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 4ce1c0e39d6de..487ffaffa95e9 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -7,6 +7,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\DriverPool; /** @@ -34,13 +36,6 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ protected $_tmpDir = ''; - /** - * Download directory for url-based resources. - * - * @var string - */ - private $downloadDir; - /** * Destination directory. * @@ -111,13 +106,18 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ private $random; + /** + * @var Filesystem + */ + private $fileSystem; + /** * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb * @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage * @param \Magento\Framework\Image\AdapterFactory $imageFactory * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Framework\Filesystem\File\ReadFactory $readFactory + * @param Filesystem $filesystem + * @param Filesystem\File\ReadFactory $readFactory * @param string|null $filePath * @param \Magento\Framework\Math\Random|null $random * @throws \Magento\Framework\Exception\FileSystemException @@ -128,8 +128,8 @@ public function __construct( \Magento\MediaStorage\Helper\File\Storage $coreFileStorage, \Magento\Framework\Image\AdapterFactory $imageFactory, \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator, - \Magento\Framework\Filesystem $filesystem, - \Magento\Framework\Filesystem\File\ReadFactory $readFactory, + Filesystem $filesystem, + Filesystem\File\ReadFactory $readFactory, $filePath = null, \Magento\Framework\Math\Random $random = null ) { @@ -137,13 +137,13 @@ public function __construct( $this->_coreFileStorageDb = $coreFileStorageDb; $this->_coreFileStorage = $coreFileStorage; $this->_validator = $validator; + $this->fileSystem = $filesystem; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $this->_readFactory = $readFactory; if ($filePath !== null) { $this->_setUploadFile($filePath); } $this->random = $random ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class); - $this->downloadDir = DirectoryList::getDefaultConfig()[DirectoryList::TMP][DirectoryList::PATH]; } /** @@ -179,8 +179,7 @@ public function move($fileName, $renameFileOff = false) $driver = ($matches[0] === $this->httpScheme) ? DriverPool::HTTP : DriverPool::HTTPS; $tmpFilePath = $this->downloadFileFromUrl($url, $driver); } else { - $tmpDir = $this->getTmpDir() ? ($this->getTmpDir() . '/') : ''; - $tmpFilePath = $this->_directory->getRelativePath($tmpDir . $fileName); + $tmpFilePath = $this->_directory->getRelativePath($this->getTempFilePath($fileName)); } $this->_setUploadFile($tmpFilePath); @@ -217,8 +216,13 @@ private function downloadFileFromUrl($url, $driver) $tmpFileName = str_replace(".$fileExtension", '', $fileName); $tmpFileName .= '_' . $this->random->getRandomString(16); $tmpFileName .= $fileExtension ? ".$fileExtension" : ''; - $tmpFilePath = $this->_directory->getRelativePath($this->downloadDir . '/' . $tmpFileName); + $tmpFilePath = $this->_directory->getRelativePath($this->getTempFilePath($tmpFileName)); + if (!$this->_directory->isWritable($this->getTmpDir())) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Import images directory must be writable in order to process remote images.') + ); + } $this->_directory->writeFile( $tmpFilePath, $this->_readFactory->create($url, $driver)->readAll() @@ -236,7 +240,20 @@ private function downloadFileFromUrl($url, $driver) */ protected function _setUploadFile($filePath) { - if (!$this->_directory->isReadable($filePath)) { + try { + $fullPath = $this->_directory->getAbsolutePath($filePath); + if ($this->getTmpDir()) { + $tmpDir = $this->fileSystem->getDirectoryReadByPath( + $this->_directory->getAbsolutePath($this->getTmpDir()) + ); + } else { + $tmpDir = $this->_directory; + } + $readable = $tmpDir->isReadable($fullPath); + } catch (ValidatorException $exception) { + $readable = false; + } + if (!$readable) { throw new \Magento\Framework\Exception\LocalizedException( __('File \'%1\' was not found or has read restriction.', $filePath) ); @@ -381,6 +398,19 @@ protected function _moveFile($tmpPath, $destPath) } } + /** + * Append temp path to filename + * + * @param string $filename + * @return string + */ + private function getTempFilePath(string $filename): string + { + return $this->getTmpDir() + ? rtrim($this->getTmpDir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename + : $filename; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml index f792b0be2eb6b..c56bc667e2494 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml @@ -26,7 +26,7 @@ <waitForPageLoad stepKey="waitForUserInput"/> <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue"/> <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> </actionGroup> <!-- Export products without filtering --> @@ -41,7 +41,7 @@ <wait stepKey="waitForScroll" time="5"/> <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> <wait stepKey="waitForClick" time="5"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue, wait to get your file soon. Make sure your cron job is running to export the file" stepKey="seeSuccessMessage"/> </actionGroup> <!-- Download first file in the grid --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml new file mode 100644 index 0000000000000..f0ec7dbd0706b --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml @@ -0,0 +1,222 @@ +<?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="AdminExportImportConfigurableProductWithImagesTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Export/Import products"/> + <title value="Check importing of configurable products with images present in filesystem"/> + <description value="Check importing of configurable products with images present in filesystem"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11557"/> + <group value="configurable_product"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create sample data: + 1. Create simple products --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + + <!-- 2. Create Downloadable product --> + <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> + <createData entity="ApiDownloadableLink" stepKey="addFirstDownloadableLink"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + <createData entity="ApiDownloadableLink" stepKey="addSecondDownloadableLink"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + + <!-- 3. Create Grouped product --> + <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + + <!-- 4. Create configurable product with images --> + <createData entity="CategoryExportImport" stepKey="createExportImportCategory"/> + <createData entity="ApiConfigurableExportImportProduct" stepKey="createExportImportConfigurableProduct"> + <requiredEntity createDataKey="createExportImportCategory"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryForExportImport" stepKey="createConfigurableProductWithImage"> + <requiredEntity createDataKey="createExportImportConfigurableProduct"/> + </createData> + <createData entity="ProductAttributeWithTwoOptionsForExportImport" stepKey="createExportImportConfigurableProductAttribute"/> + <createData entity="ProductAttributeOptionOneForExportImport" stepKey="createExportImportConfigurableProductAttributeFirstOption"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + </createData> + <createData entity="ProductAttributeOptionTwoForExportImport" stepKey="createExportImportConfigurableProductAttributeSecondOption"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + </getData> + <createData entity="ApiSimpleOneExportImport" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryForExportImport" stepKey="addImageForFirstSimpleProduct"> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ApiSimpleTwoExportImport" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="addImageForSecondSimpleProduct"> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createExportImportConfigurableProductTwoOption"> + <requiredEntity createDataKey="createExportImportConfigurableProduct"/> + <requiredEntity createDataKey="createExportImportConfigurableProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="addFirstExportImportConfigurableProductChild"> + <requiredEntity createDataKey="createExportImportConfigurableProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="addSecondExportImportConfigurableProductChild"> + <requiredEntity createDataKey="createExportImportConfigurableProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- 5. Create configurable product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttr"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttr"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttrSet"> + <requiredEntity createDataKey="createConfigProductAttr"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttr"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttr"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createConfigProductAttr"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="addConfigurableProductChild"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createConfigChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllExportedFiles" stepKey="clearExportedFilesList"/> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete created data --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFisrtSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + <deleteData createDataKey="createExportImportConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createExportImportConfigurableProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> + <deleteData createDataKey="createConfigProductAttr" stepKey="deleteConfigProductAttr"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createExportImportCategory" stepKey="deleteExportImportCategory"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> + <!-- Admin logout--> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Go to System > Export --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + + <!-- Set Export Settings: Entity Type > Products, SKU > ConfProd's sku and press "Continue" --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createExportImportConfigurableProduct.sku$$"/> + </actionGroup> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCronFirstTime"/> + <magentoCLI command="cron:run" stepKey="runCronSecondTime"/> + + <!-- Save exported file: file successfully downloaded --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Go to Catalog > Products. Find ConfProd and delete it --> + <actionGroup ref="deleteProductBySku" stepKey="deleteConfigurableProductBySku"> + <argument name="sku" value="$$createExportImportConfigurableProduct.sku$$"/> + </actionGroup> + + <!-- Go to System > Import. Set import settings: Entity Type > Product, Import Behavior > Add/Update, + Select File to Import > previously exported file and press "Check Data" --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="export_import_configurable_product.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + </actionGroup> + + <!-- Go to Catalog > Products: Configurable product exists --> + <actionGroup ref="filterAndSelectProduct" stepKey="openConfigurableProduct"> + <argument name="productSku" value="$$createExportImportConfigurableProduct.sku$$"/> + </actionGroup> + + <!-- Go to "Configurations" section: configurations exist and have images --> + <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="2" stepKey="seeNumberOfRows"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="$$createConfigFirstChildProduct.name$$" stepKey="seeFirstProductNameInField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="$$createConfigSecondChildProduct.name$$" stepKey="seeSecondProductNameInField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="$$createConfigFirstChildProduct.sku$$" stepKey="seeFirstProductSkuInField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="$$createConfigSecondChildProduct.sku$$" stepKey="seeSecondProductSkuInField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="$$createConfigFirstChildProduct.price$$" stepKey="seeFirstProductPriceInField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="$$createConfigSecondChildProduct.price$$" stepKey="seeSecondProductPriceInField"/> + <seeElement selector="{{AdminProductFormConfigurationsSection.variationImageSource(MagentoLogo.fileName)}}" stepKey="seeFirstProductImageInField"/> + <seeElement selector="{{AdminProductFormConfigurationsSection.variationImageSource(TestImage.fileName)}}" stepKey="seeSecondProductImageInField"/> + + <!-- Go to "Images and Videos" section: assert image --> + <scrollTo selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" stepKey="scrollToProductGalleryTab"/> + <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!-- Go to any ConfProd's configuration page: Product page open successfully --> + <click selector="{{AdminProductFormConfigurationsSection.variationProductLinkByName($$createConfigFirstChildProduct.name$$)}}" stepKey="clickOnFirstProductLink"/> + <switchToNextTab stepKey="switchToConfigChildProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <!-- Go to "Images and Videos" section: assert image --> + <scrollTo selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" stepKey="scrollToChildProductGalleryTab"/> + <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertChildProductImageAdminProductPage"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <closeTab stepKey="closeConfigChildProductPage"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index bd2fe896b8c0a..371d75bc922f3 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -72,7 +72,9 @@ protected function setUp() 'setAttributeSetFilter' ] ); - $attribute = $this->createPartialMock(\Magento\Eav\Model\Entity\Attribute::class, [ + $attribute = $this->createPartialMock( + \Magento\Eav\Model\Entity\Attribute::class, + [ 'getAttributeCode', 'getId', 'getIsVisible', @@ -85,7 +87,8 @@ protected function setUp() 'getDefaultValue', 'usesSource', 'getFrontendInput', - ]); + ] + ); $attribute->expects($this->any())->method('getIsVisible')->willReturn(true); $attribute->expects($this->any())->method('getIsGlobal')->willReturn(true); $attribute->expects($this->any())->method('getIsRequired')->willReturn(true); @@ -107,6 +110,7 @@ protected function setUp() ]; $attribute1 = clone $attribute; $attribute2 = clone $attribute; + $attribute3 = clone $attribute; $attribute1->expects($this->any())->method('getId')->willReturn('1'); $attribute1->expects($this->any())->method('getAttributeCode')->willReturn('attr_code'); @@ -118,6 +122,11 @@ protected function setUp() $attribute2->expects($this->any())->method('getFrontendInput')->willReturn('boolean'); $attribute2->expects($this->any())->method('isStatic')->willReturn(false); + $attribute3->expects($this->any())->method('getId')->willReturn('3'); + $attribute3->expects($this->any())->method('getAttributeCode')->willReturn('text_attribute'); + $attribute3->expects($this->any())->method('getFrontendInput')->willReturn('text'); + $attribute3->expects($this->any())->method('isStatic')->willReturn(false); + $this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3); $this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturnOnConsecutiveCalls( ['option1', 'option2'], @@ -126,7 +135,9 @@ protected function setUp() $attrSetColFactory->expects($this->any())->method('create')->willReturn($attrSetCollection); $attrSetCollection->expects($this->any())->method('setEntityTypeFilter')->willReturn([$attributeSet]); $attrColFactory->expects($this->any())->method('create')->willReturn($attrCollection); - $attrCollection->expects($this->any())->method('setAttributeSetFilter')->willReturn([$attribute1, $attribute2]); + $attrCollection->expects($this->any()) + ->method('setAttributeSetFilter') + ->willReturn([$attribute1, $attribute2, $attribute3]); $attributeSet->expects($this->any())->method('getId')->willReturn(1); $attributeSet->expects($this->any())->method('getAttributeSetName')->willReturn('attribute_set_name'); @@ -157,9 +168,11 @@ protected function setUp() ], ] ) - ->willReturn([$attribute1, $attribute2]); + ->willReturn([$attribute1, $attribute2, $attribute3]); - $this->connection = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, [ + $this->connection = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + [ 'select', 'fetchAll', 'fetchPairs', @@ -167,13 +180,17 @@ protected function setUp() 'insertOnDuplicate', 'delete', 'quoteInto' - ]); - $this->select = $this->createPartialMock(\Magento\Framework\DB\Select::class, [ + ] + ); + $this->select = $this->createPartialMock( + \Magento\Framework\DB\Select::class, + [ 'from', 'where', 'joinLeft', 'getConnection', - ]); + ] + ); $this->select->expects($this->any())->method('from')->will($this->returnSelf()); $this->select->expects($this->any())->method('where')->will($this->returnSelf()); $this->select->expects($this->any())->method('joinLeft')->will($this->returnSelf()); @@ -189,10 +206,13 @@ protected function setUp() ->method('fetchAll') ->will($this->returnValue($entityAttributes)); - $this->resource = $this->createPartialMock(\Magento\Framework\App\ResourceConnection::class, [ + $this->resource = $this->createPartialMock( + \Magento\Framework\App\ResourceConnection::class, + [ 'getConnection', 'getTableName', - ]); + ] + ); $this->resource->expects($this->any())->method('getConnection')->will( $this->returnValue($this->connection) ); @@ -257,9 +277,13 @@ public function testIsRowValidSuccess() $rowNum = 1; $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(null); $this->entityModel->expects($this->never())->method('addRowError'); - $this->setPropertyValue($this->simpleType, '_attributes', [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [], - ]); + $this->setPropertyValue( + $this->simpleType, + '_attributes', + [ + $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [], + ] + ); $this->assertTrue($this->simpleType->isRowValid($rowData, $rowNum)); } @@ -278,13 +302,17 @@ public function testIsRowValidError() 'attr_code' ) ->willReturnSelf(); - $this->setPropertyValue($this->simpleType, '_attributes', [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [ - 'attr_code' => [ - 'is_required' => true, + $this->setPropertyValue( + $this->simpleType, + '_attributes', + [ + $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [ + 'attr_code' => [ + 'is_required' => true, + ], ], - ], - ]); + ] + ); $this->assertFalse($this->simpleType->isRowValid($rowData, $rowNum)); } @@ -364,9 +392,14 @@ public function testPrepareAttributesWithDefaultValueForSave() { $rowData = [ '_attribute_set' => 'attributeSetName', - 'boolean_attribute' => 'Yes' + 'boolean_attribute' => 'Yes', + ]; + + $expected = [ + 'boolean_attribute' => 1, + 'text_attribute' => 'default_value' ]; $result = $this->simpleType->prepareAttributesWithDefaultValueForSave($rowData); - $this->assertEquals(['boolean_attribute' => 1], $result); + $this->assertEquals($expected, $result); } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index 9f63decac5ff7..f8b14a471fd9c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -776,6 +776,77 @@ public function testValidateAmbiguousData( $this->assertEquals($errors, $resultErrors); } + /** + * Test for row without store view code field + * @param array $rowData + * @param array $responseData + * + * @covers \Magento\CatalogImportExport\Model\Import\Product\Option::_parseCustomOptions + * @dataProvider validateRowStoreViewCodeFieldDataProvider + */ + public function testValidateRowDataForStoreViewCodeField($rowData, $responseData) + { + $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product\Option::class); + $reflectionMethod = $reflection->getMethod('_parseCustomOptions'); + $reflectionMethod->setAccessible(true); + $result = $reflectionMethod->invoke($this->model, $rowData); + $this->assertEquals($responseData, $result); + } + + /** + * Data provider for test of method _parseCustomOptions + * + * @return array + */ + public function validateRowStoreViewCodeFieldDataProvider() + { + return [ + 'with_store_view_code' => [ + '$rowData' => [ + 'store_view_code' => '', + 'custom_options' => + 'name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed' + ], + '$responseData' => [ + 'store_view_code' => '', + 'custom_options' => [ + 'Test Field Title' => [ + [ + 'name' => 'Test Field Title', + 'type' => 'field', + 'required' => '1', + 'sku' => '1-text', + 'price' => '0', + 'price_type' => 'fixed', + '_custom_option_store' => '' + ] + ] + ] + ], + ], + 'without_store_view_code' => [ + '$rowData' => [ + 'custom_options' => + 'name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed' + ], + '$responseData' => [ + 'custom_options' => [ + 'Test Field Title' => [ + [ + 'name' => 'Test Field Title', + 'type' => 'field', + 'required' => '1', + 'sku' => '1-text', + 'price' => '0', + 'price_type' => 'fixed' + ] + ] + ] + ], + ] + ]; + } + /** * Data provider of row data and errors * diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php new file mode 100644 index 0000000000000..e018fc0cf5ccf --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Validator; + +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\Authorization\Model\UserContextInterface; +use Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdatePermissions; +use Magento\Framework\AuthorizationInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Test validation for layout update permissions + */ +class LayoutUpdatePermissionsTest extends TestCase +{ + /** + * @var LayoutUpdatePermissions|MockObject + */ + private $validator; + + /** + * @var UserContextInterface|MockObject + */ + private $userContext; + + /** + * @var AuthorizationInterface|MockObject + */ + private $authorization; + + /** + * @var Product + */ + private $context; + + protected function setUp() + { + $this->userContext = $this->createMock(UserContextInterface::class); + $this->authorization = $this->createMock(AuthorizationInterface::class); + $this->context = $this->createMock(Product::class); + $this->context + ->method('retrieveMessageTemplate') + ->with('insufficientPermissions') + ->willReturn('oh no'); + $this->validator = new LayoutUpdatePermissions( + $this->userContext, + $this->authorization + ); + $this->validator->init($this->context); + } + + /** + * @param $value + * @param $userContext + * @param $isAllowed + * @param $isValid + * @dataProvider configurationsProvider + */ + public function testValidationConfiguration($value, $userContext, $isAllowed, $isValid) + { + $this->userContext + ->method('getUserType') + ->willReturn($userContext); + + $this->authorization + ->method('isAllowed') + ->with('Magento_Catalog::edit_product_design') + ->willReturn($isAllowed); + + $result = $this->validator->isValid(['custom_layout_update' => $value]); + $messages = $this->validator->getMessages(); + + self::assertSame($isValid, $result); + + if ($isValid) { + self::assertSame([], $messages); + } else { + self::assertSame(['oh no'], $messages); + } + } + + public function configurationsProvider() + { + return [ + ['', null, null, true], + [null, null, null, true], + ['foo', UserContextInterface::USER_TYPE_ADMIN, true, true], + ['foo', UserContextInterface::USER_TYPE_INTEGRATION, true, true], + ['foo', UserContextInterface::USER_TYPE_ADMIN, false, false], + ['foo', UserContextInterface::USER_TYPE_INTEGRATION, false, false], + ['foo', 'something', null, false], + ]; + } +} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php new file mode 100644 index 0000000000000..d1e8b879f6a08 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Validator; + +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\View\Model\Layout\Update\Validator; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Test validation for layout update + */ +class LayoutUpdateTest extends TestCase +{ + /** + * @var LayoutUpdate|MockObject + */ + private $validator; + + /** + * @var Validator|MockObject + */ + private $layoutValidator; + + protected function setUp() + { + $validatorFactory = $this->createMock(ValidatorFactory::class); + $validationState = $this->createMock(ValidationStateInterface::class); + $this->layoutValidator = $this->createMock(Validator::class); + $validatorFactory->method('create') + ->with(['validationState' => $validationState]) + ->willReturn($this->layoutValidator); + + $this->validator = new LayoutUpdate( + $validatorFactory, + $validationState + ); + } + + public function testValidationIsSkippedWithDataNotPresent() + { + $this->layoutValidator + ->expects($this->never()) + ->method('isValid'); + + $result = $this->validator->isValid([]); + self::assertTrue($result); + } + + public function testValidationFailsProperly() + { + $this->layoutValidator + ->method('isValid') + ->with('foo') + ->willReturn(false); + + $contextMock = $this->createMock(Product::class); + $contextMock + ->method('retrieveMessageTemplate') + ->with('invalidLayoutUpdate') + ->willReturn('oh no'); + $this->validator->init($contextMock); + + $result = $this->validator->isValid(['custom_layout_update' => 'foo']); + $messages = $this->validator->getMessages(); + self::assertFalse($result); + self::assertSame(['oh no'], $messages); + } + + public function testInvalidDataException() + { + $this->layoutValidator + ->method('isValid') + ->willThrowException(new \Exception('foo')); + + $contextMock = $this->createMock(Product::class); + $contextMock + ->method('retrieveMessageTemplate') + ->with('invalidLayoutUpdate') + ->willReturn('oh no'); + $this->validator->init($contextMock); + + $result = $this->validator->isValid(['custom_layout_update' => 'foo']); + $messages = $this->validator->getMessages(); + self::assertFalse($result); + self::assertSame(['oh no'], $messages); + } +} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index f85d33edb5d8c..40041fe90db96 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -284,9 +284,11 @@ protected function setUp() ->getMock(); $this->storeResolver = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product\StoreResolver::class) - ->setMethods([ - 'getStoreCodeToId', - ]) + ->setMethods( + [ + 'getStoreCodeToId', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->skuProcessor = @@ -410,7 +412,7 @@ protected function _objectConstructor() $this->_filesystem->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::ROOT) - ->will($this->returnValue(self::MEDIA_DIRECTORY)); + ->willReturn($this->_mediaDirectory); $this->validator->expects($this->any())->method('init'); return $this; @@ -596,9 +598,13 @@ public function testGetMultipleValueSeparatorDefault() public function testGetMultipleValueSeparatorFromParameters() { $expectedSeparator = 'value'; - $this->setPropertyValue($this->importProduct, '_parameters', [ - \Magento\ImportExport\Model\Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR => $expectedSeparator, - ]); + $this->setPropertyValue( + $this->importProduct, + '_parameters', + [ + \Magento\ImportExport\Model\Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR => $expectedSeparator, + ] + ); $this->assertEquals( $expectedSeparator, @@ -618,9 +624,13 @@ public function testGetEmptyAttributeValueConstantDefault() public function testGetEmptyAttributeValueConstantFromParameters() { $expectedSeparator = '__EMPTY__VALUE__TEST__'; - $this->setPropertyValue($this->importProduct, '_parameters', [ - \Magento\ImportExport\Model\Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT => $expectedSeparator, - ]); + $this->setPropertyValue( + $this->importProduct, + '_parameters', + [ + \Magento\ImportExport\Model\Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT => $expectedSeparator, + ] + ); $this->assertEquals( $expectedSeparator, @@ -632,9 +642,12 @@ public function testDeleteProductsForReplacement() { $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods([ - 'setParameters', '_deleteProducts' - ]) + ->setMethods( + [ + 'setParameters', + '_deleteProducts' + ] + ) ->getMock(); $importProduct->expects($this->once())->method('setParameters')->with( @@ -764,9 +777,13 @@ public function testGetProductWebsites() 'key 3' => 'val', ]; $expectedResult = array_keys($productValue); - $this->setPropertyValue($this->importProduct, 'websitesCache', [ - $productSku => $productValue - ]); + $this->setPropertyValue( + $this->importProduct, + 'websitesCache', + [ + $productSku => $productValue + ] + ); $actualResult = $this->importProduct->getProductWebsites($productSku); @@ -785,9 +802,13 @@ public function testGetProductCategories() 'key 3' => 'val', ]; $expectedResult = array_keys($productValue); - $this->setPropertyValue($this->importProduct, 'categoriesCache', [ - $productSku => $productValue - ]); + $this->setPropertyValue( + $this->importProduct, + 'categoriesCache', + [ + $productSku => $productValue + ] + ); $actualResult = $this->importProduct->getProductCategories($productSku); @@ -1112,9 +1133,13 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() ->disableOriginalConstructor() ->getMock(); $productType->expects($this->once())->method('isRowValid')->with($expectedRowData); - $this->setPropertyValue($importProduct, '_productTypeModels', [ - $newSku['type_id'] => $productType - ]); + $this->setPropertyValue( + $importProduct, + '_productTypeModels', + [ + $newSku['type_id'] => $productType + ] + ); //suppress option validation $this->_rewriteGetOptionEntityInImportProduct($importProduct); @@ -1229,6 +1254,56 @@ public function testParseAttributesWithWrappedValuesWillReturnsLowercasedAttribu $this->assertArrayNotHasKey('PARAM2', $attributes); } + /** + * @param bool $isRead + * @param bool $isWrite + * @param string $message + * @dataProvider fillUploaderObjectDataProvider + */ + public function testFillUploaderObject($isRead, $isWrite, $message) + { + $fileUploaderMock = $this + ->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Uploader::class) + ->disableOriginalConstructor() + ->getMock(); + + $fileUploaderMock + ->method('setTmpDir') + ->with('pub/media/import') + ->willReturn($isRead); + + $fileUploaderMock + ->method('setDestDir') + ->with('pub/media/catalog/product') + ->willReturn($isWrite); + + $this->_mediaDirectory + ->method('getRelativePath') + ->willReturnMap( + [ + ['import', 'import'], + ['catalog/product', 'catalog/product'], + ] + ); + + $this->_mediaDirectory + ->method('create') + ->with('pub/media/catalog/product'); + + $this->_uploaderFactory + ->expects($this->once()) + ->method('create') + ->willReturn($fileUploaderMock); + + try { + $this->importProduct->getUploader(); + $this->assertNotNull($this->getPropertyValue($this->importProduct, '_fileUploader')); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->assertNull($this->getPropertyValue($this->importProduct, '_fileUploader')); + $this->assertEquals($message, $e->getMessage()); + } + } + /** * Test that errors occurred during importing images are logged. * @@ -1275,6 +1350,20 @@ function ($name) use ($throwException, $exception) { ); } + /** + * Data provider for testFillUploaderObject. + * + * @return array + */ + public function fillUploaderObjectDataProvider() + { + return [ + [false, true, 'File directory \'pub/media/import\' is not readable.'], + [true, false, 'File directory \'pub/media/catalog/product\' is not writable.'], + [true, true, ''], + ]; + } + /** * Data provider for testUploadMediaFiles. * diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index 2c6aa6535c10e..f10cf0364c545 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -128,6 +128,7 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $che { $tmpDir = 'var/tmp'; $destDir = 'var/dest/dir'; + $this->uploader->method('getTmpDir')->willReturn($tmpDir); // Expected invocation to validate file extension $this->uploader->expects($this->exactly($checkAllowedExtension))->method('checkAllowedExtension') @@ -159,9 +160,11 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $che $this->directoryMock->expects($this->any())->method('writeFile') ->will($this->returnValue($expectedFileName)); - // Expected invocations to move the temp file to the destination directory - $this->directoryMock->expects($this->once())->method('isWritable') - ->with($destDir) + // Expected invocations save the downloaded file to temp file + // and move the temp file to the destination directory + $this->directoryMock->expects($this->exactly(2)) + ->method('isWritable') + ->withConsecutive([$destDir], [$tmpDir]) ->willReturn(true); $this->directoryMock->expects($this->once())->method('getAbsolutePath') ->with($destDir) @@ -172,9 +175,6 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $che ->with($destDir . '/' . $expectedFileName) ->willReturn(['name' => $expectedFileName, 'path' => 'absPath']); - // Do not use configured temp directory - $this->uploader->expects($this->never())->method('getTmpDir'); - $this->uploader->setDestDir($destDir); $result = $this->uploader->move($fileUrl); diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index 6af2bbaf45e3c..25d9c1bde0d68 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -16,7 +16,8 @@ "magento/module-import-export": "*", "magento/module-media-storage": "*", "magento/module-store": "*", - "magento/module-tax": "*" + "magento/module-tax": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml index 6906272b11d68..4e2fe390e0b17 100644 --- a/app/code/Magento/CatalogImportExport/etc/di.xml +++ b/app/code/Magento/CatalogImportExport/etc/di.xml @@ -25,7 +25,14 @@ <item name="website" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\Website</item> <item name="weight" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\Weight</item> <item name="quantity" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\Quantity</item> + <item name="layout_update" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate</item> + <item name="layout_update_permissions" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdatePermissions</item> </argument> </arguments> </type> + <type name="Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate"> + <arguments> + <argument name="validationState" xsi:type="object">Magento\Framework\Config\ValidationState\Required</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php index 6c4f6a0f46a59..ffcb758dcbd66 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php @@ -37,7 +37,7 @@ protected function _getGroupRenderer() '', ['data' => ['is_render_to_js_template' => true]] ); - $this->_groupRenderer->setClass('customer_group_select'); + $this->_groupRenderer->setClass('customer_group_select admin__control-select'); } return $this->_groupRenderer; } @@ -57,7 +57,7 @@ protected function _prepareToRender() 'min_sale_qty', [ 'label' => __('Minimum Qty'), - 'class' => 'required-entry validate-number validate-greater-than-zero' + 'class' => 'required-entry validate-number validate-greater-than-zero admin__control-text' ] ); $this->_addAfter = false; diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php index f9a49d4f8d121..35231b8460b19 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -104,6 +104,10 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = $select->where('stock_item.use_config_manage_stock = 0 AND stock_item.manage_stock = 1'); } + if (!empty($entityIds)) { + $select->where('stock_item.product_id in (?)', $entityIds); + } + $select->group('stock_item.product_id'); $select->having('max_is_in_stock = 0'); diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index 3670b93b8cb48..c5644060c689f 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -230,6 +230,8 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f { $connection = $this->getConnection(); $qtyExpr = $connection->getCheckSql('cisi.qty > 0', 'cisi.qty', 0); + $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $linkField = $metadata->getLinkField(); $select = $connection->select()->from( ['e' => $this->getTable('catalog_product_entity')], @@ -243,6 +245,12 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f ['cisi' => $this->getTable('cataloginventory_stock_item')], 'cisi.stock_id = cis.stock_id AND cisi.product_id = e.entity_id', [] + )->joinInner( + ['mcpei' => $this->getTable('catalog_product_entity_int')], + 'e.' . $linkField . ' = mcpei.' . $linkField + . ' AND mcpei.attribute_id = ' . $this->_getAttribute('status')->getId() + . ' AND mcpei.value = ' . ProductStatus::STATUS_ENABLED, + [] )->columns( ['qty' => $qtyExpr] )->where( diff --git a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index 7f43cd279d4e3..0000000000000 --- a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogInventory\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tableName = $this->schemaSetup->getTable('cataloginventory_stock_status_tmp'); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml new file mode 100644 index 0000000000000..49956473132ec --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup"> + <arguments> + <argument name="qty" type="string"/> + <argument name="errorMessage" type="string"/> + </arguments> + + <fillField selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQty}}" userInput="{{qty}}" stepKey="setMaxSaleQtyValue"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <waitForElementVisible selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQtyError}}" stepKey="waitValidationErrorMessageAppears"/> + <see selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQtyError}}" userInput="{{errorMessage}}" stepKey="checkValidationErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml new file mode 100644 index 0000000000000..84dc6b93c885f --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.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"> + <actionGroup name="AdminProductSetMaxQtyAllowedInShoppingCart"> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductFormSection.advancedInventoryLink}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.advancedInventoryModal}}" visible="false" stepKey="clickOnAdvancedInventoryLinkIfNeeded"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="waitForAdvancedInventoryModalWindowOpen"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="{{qty}}" stepKey="fillMaxAllowedQty"/> + <click selector="{{AdminSlideOutDialogSection.doneButton}}" stepKey="clickDone"/> + </actionGroup> + + <actionGroup name="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" extends="AdminProductSetMaxQtyAllowedInShoppingCart"> + <arguments> + <argument name="qty" type="string"/> + <argument name="errorMessage" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCartError}}" after="clickDone" stepKey="waitProductValidationErrorMessageAppears"/> + <see selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCartError}}" userInput="{{errorMessage}}" after="waitProductValidationErrorMessageAppears" stepKey="checkProductValidationErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml index e14c36446fc2b..cd5a8cf5bbac9 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml @@ -20,4 +20,17 @@ <data key="label">No</data> <data key="value">0</data> </entity> + <entity name="EnableCatalogInventoryConfigData"> + <!--Default Value --> + <data key="path">cataloginventory/options/can_subtract</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableCatalogInventoryConfigData"> + <data key="path">cataloginventory/options/can_subtract</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml new file mode 100644 index 0000000000000..767d65f9facca --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml @@ -0,0 +1,17 @@ +<?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="DefaultValueForMaxSaleQty" type="cataloginventory_item_options"> + <requiredEntity type="max_sale_qty">MaxSaleQtyDefaultValue</requiredEntity> + </entity> + <entity name="MaxSaleQtyDefaultValue" type="max_sale_qty"> + <data key="value">10000</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml new file mode 100644 index 0000000000000..7672cb7478f1a --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogInventoryProductStockOptionsConfiguration" dataType="cataloginventory_item_options" type="create" + auth="adminFormKey" url="/admin/system_config/save/section/cataloginventory/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="cataloginventory_item_options"> + <object key="item_options" dataType="cataloginventory_item_options"> + <object key="fields" dataType="cataloginventory_item_options"> + <object key="max_sale_qty" dataType="max_sale_qty"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml new file mode 100644 index 0000000000000..3d8c3ef3cf9f8 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.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="AdminInventoryProductStockOptionsConfigPage" url="admin/system_config/edit/section/cataloginventory/#cataloginventory_item_options-link" area="admin" module="Magento_Config"> + <section name="AdminInventoryProductStockOptionsConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..5835e7564c172 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.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="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminProductFormAdvancedInventorySection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml new file mode 100644 index 0000000000000..ef7fe30f4970b --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.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="AdminInventoryProductStockOptionsConfigSection"> + <element name="maxSaleQtyInherit" type="checkbox" selector="#cataloginventory_item_options_max_sale_qty_inherit" timeout="30"/> + <element name="maxSaleQty" type="input" selector="#cataloginventory_item_options_max_sale_qty"/> + <element name="maxSaleQtyError" type="input" selector="#cataloginventory_item_options_max_sale_qty-error"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml similarity index 92% rename from app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml rename to app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml index 4196a86fe25db..7ff9c2d70755f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -30,5 +30,7 @@ <element name="advancedInventoryStockStatus" type="select" selector="//div[@class='modal-inner-wrap']//select[@name='product[quantity_and_stock_status][is_in_stock]']"/> <element name="outOfStockThreshold" type="select" selector="//*[@name='product[stock_data][min_qty]']" timeout="30"/> <element name="minQtyConfigSetting" type="checkbox" selector="//input[@name='product[stock_data][use_config_min_qty]']" timeout="30"/> + <element name="advancedInventoryModal" type="block" selector=".product_form_product_form_advanced_inventory_modal[data-role=modal]"/> + <element name="maxiQtyAllowedInCartError" type="text" selector="[name='product[stock_data][max_sale_qty]'] + label.admin__field-error"/> </section> </sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml new file mode 100644 index 0000000000000..945613ee753d6 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.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="AdminProductFormSection"> + <element name="advancedInventoryLink" type="button" selector="button[data-index='advanced_inventory_button'].action-additional" timeout="30"/> + <element name="advancedInventoryButton" type="button" selector="button[data-index='advanced_inventory_button'].action-basic" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml new file mode 100644 index 0000000000000..f7cf0a4deba4b --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml @@ -0,0 +1,80 @@ +<?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="AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest"> + <annotations> + <features value="CatalogInventory"/> + <stories value="Sales restrictions"/> + <title value="Verify that product maximum qty allowed in shopping cart can't be set to zero or less"/> + <description value="Verify that product maximum qty allowed in shopping cart can't be set to zero or less"/> + <severity value="MAJOR"/> + <useCaseId value="MC-17606"/> + <testCaseId value="MC-17636"/> + <group value="catalog"/> + <group value="catalogInventory"/> + </annotations> + <before> + <createData entity="DefaultValueForMaxSaleQty" stepKey="setDefaultValueForMaxSaleQty"/> + <createData entity="SimpleProduct2" stepKey="createdProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <createData entity="DefaultValueForMaxSaleQty" stepKey="setDefaultValueForMaxSaleQty"/> + <deleteData createDataKey="createdProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to Inventory configuration page --> + <amOnPage url="{{AdminInventoryProductStockOptionsConfigPage.url}}" stepKey="openInventoryConfigPage"/> + <uncheckOption selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQtyInherit}}" stepKey="uncheckUseDefaultValueForMaxSaleQty"/> + <!-- Validate zero value --> + <actionGroup ref="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="validateZeroValue"> + <argument name="qty" value="0"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate negative value --> + <actionGroup ref="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="validateNegativeValue"> + <argument name="qty" value="-1"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate alphabetical value --> + <actionGroup ref="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="validateAlphabeticalValue"> + <argument name="qty" value="abc"/> + <argument name="errorMessage" value="Please enter a valid number in this field."/> + </actionGroup> + <!-- Fill correct value --> + <fillField selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQty}}" userInput="100" stepKey="setMaxSaleQtyValueToCorrectNumber"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigWithCorrectNumber"/> + + <!-- Go to product page --> + <amOnPage url="{{AdminProductEditPage.url($$createdProduct.id$$)}}" stepKey="openAdminProductEditPage"/> + <!-- Validate zero value --> + <actionGroup ref="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="productValidateZeroValue"> + <argument name="qty" value="0"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate negative value --> + <actionGroup ref="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="productValidateNegativeValue"> + <argument name="qty" value="-1"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate alphabetical value --> + <actionGroup ref="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="productValidateAlphabeticalValue"> + <argument name="qty" value="abc"/> + <argument name="errorMessage" value="Please enter a valid number in this field."/> + </actionGroup> + <!-- Fill correct value --> + <actionGroup ref="AdminProductSetMaxQtyAllowedInShoppingCart" stepKey="setProductMaxQtyAllowedInShoppingCartToCorrectNumber"> + <argument name="qty" value="50"/> + </actionGroup> + <waitForElementNotVisible selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryModal}}" stepKey="waitForModalFormToDisappear"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php new file mode 100644 index 0000000000000..46f4e0f26f378 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogInventory\Test\Unit\Model\Indexer; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Indexer\ProductPriceIndexFilter; +use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; + +/** + * Product Price filter test, to ensure that product id's filtered. + */ +class ProductPriceIndexFilterTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var MockObject|StockConfigurationInterface $stockConfiguration + */ + private $stockConfiguration; + + /** + * @var MockObject|Item $item + */ + private $item; + + /** + * @var MockObject|ResourceConnection $resourceCnnection + */ + private $resourceCnnection; + + /** + * @var MockObject|Generator $generator + */ + private $generator; + + /** + * @var ProductPriceIndexFilter $productPriceIndexFilter + */ + private $productPriceIndexFilter; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->stockConfiguration = $this->createMock(StockConfigurationInterface::class); + $this->item = $this->createMock(Item::class); + $this->resourceCnnection = $this->createMock(ResourceConnection::class); + $this->generator = $this->createMock(Generator::class); + + $this->productPriceIndexFilter = new ProductPriceIndexFilter( + $this->stockConfiguration, + $this->item, + $this->resourceCnnection, + 'indexer', + $this->generator, + 100 + ); + } + + /** + * Test to ensure that Modify Price method uses entityIds, + */ + public function testModifyPrice() + { + $entityIds = [1, 2, 3]; + $indexTableStructure = $this->createMock(IndexTableStructure::class); + $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + $this->resourceCnnection->expects($this->once())->method('getConnection')->willReturn($connectionMock); + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $connectionMock->expects($this->once())->method('select')->willReturn($selectMock); + $selectMock->expects($this->at(2)) + ->method('where') + ->with('stock_item.product_id in (?)', $entityIds) + ->willReturn($selectMock); + $this->generator->expects($this->once()) + ->method('generate') + ->will( + $this->returnCallback( + $this->getBatchIteratorCallback($selectMock, 5) + ) + ); + + $fetchStmtMock = $this->createPartialMock(\Zend_Db_Statement_Pdo::class, ['fetchAll']); + $fetchStmtMock->expects($this->any()) + ->method('fetchAll') + ->will($this->returnValue([['product_id' => 1]])); + $connectionMock->expects($this->any())->method('query')->will($this->returnValue($fetchStmtMock)); + $this->productPriceIndexFilter->modifyPrice($indexTableStructure, $entityIds); + } + + /** + * Returns batches. + * + * @param MockObject $selectMock + * @param int $batchCount + * @return \Closure + */ + private function getBatchIteratorCallback(MockObject $selectMock, int $batchCount): \Closure + { + $iteratorCallback = function () use ($batchCount, $selectMock): array { + $result = []; + $count = $batchCount; + while ($count) { + $count--; + $result[$count] = $selectMock; + } + + return $result; + }; + + return $iteratorCallback; + } +} diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index da465f5bdd3dc..aafde14a28584 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -1,7 +1,4 @@ <?php - -declare(strict_types=1); - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -178,7 +175,7 @@ public function modifyMeta(array $meta) } /** - * Prepare Meta + * Modify UI Quantity and Stock status attribute meta. * * @return void */ @@ -188,10 +185,6 @@ private function prepareMeta() $pathField = $this->arrayManager->findPath($fieldCode, $this->meta, null, 'children'); if ($pathField) { - $labelField = $this->arrayManager->get( - $this->arrayManager->slicePath($pathField, 0, -2) . '/arguments/data/config/label', - $this->meta - ); $fieldsetPath = $this->arrayManager->slicePath($pathField, 0, -4); $this->meta = $this->arrayManager->merge( @@ -219,10 +212,9 @@ private function prepareMeta() 'formElement' => 'container', 'componentType' => 'container', 'component' => "Magento_Ui/js/form/components/group", - 'label' => $labelField, + 'label' => false, 'breakLine' => false, 'dataScope' => $fieldCode, - 'scopeLabel' => '[GLOBAL]', 'source' => 'product_details', 'sortOrder' => (int) $this->arrayManager->get( $this->arrayManager->slicePath($pathField, 0, -2) . '/arguments/data/config/sortOrder', @@ -230,86 +222,58 @@ private function prepareMeta() ) - 1, 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ]; + $qty['arguments']['data']['config'] = [ + 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer', + 'group' => 'quantity_and_stock_status_qty', + 'dataType' => 'number', + 'formElement' => 'input', + 'componentType' => 'field', + 'visible' => '1', + 'require' => '0', + 'additionalClasses' => 'admin__field-small', + 'label' => __('Quantity'), + 'scopeLabel' => '[GLOBAL]', + 'dataScope' => 'qty', + 'validation' => [ + 'validate-number' => true, + 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, + ], + 'imports' => [ + 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal', + ], + 'sortOrder' => 10, + ]; + $advancedInventoryButton['arguments']['data']['config'] = [ + 'displayAsLink' => true, + 'formElement' => 'container', + 'componentType' => 'container', + 'component' => 'Magento_Ui/js/form/components/button', + 'template' => 'ui/form/components/button/container', + 'actions' => [ + [ + 'targetName' => 'product_form.product_form.advanced_inventory_modal', + 'actionName' => 'toggleModal', + ], + ], + 'imports' => [ + 'childError' => 'product_form.product_form.advanced_inventory_modal.stock_data:error', + ], + 'title' => __('Advanced Inventory'), + 'provider' => false, + 'additionalForGroup' => true, + 'source' => 'product_details', + 'sortOrder' => 20, + ]; $container['children'] = [ - 'qty' => $this->getQtyMetaStructure(), - 'advanced_inventory_button' => $this->getAdvancedInventoryButtonMetaStructure(), + 'qty' => $qty, + 'advanced_inventory_button' => $advancedInventoryButton, ]; $this->meta = $this->arrayManager->merge( $fieldsetPath . '/children', $this->meta, - ['container_quantity_and_stock_status_qty' => $container] + ['quantity_and_stock_status_qty' => $container] ); } } - - /** - * Get Qty meta structure - * - * @return array - */ - private function getQtyMetaStructure() - { - return [ - 'arguments' => [ - 'data' => [ - 'config' => [ - 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer', - 'group' => 'quantity_and_stock_status_qty', - 'dataType' => 'number', - 'formElement' => 'input', - 'componentType' => 'field', - 'visible' => '1', - 'require' => '0', - 'additionalClasses' => 'admin__field-small', - 'label' => __('Quantity'), - 'scopeLabel' => '[GLOBAL]', - 'dataScope' => 'qty', - 'validation' => [ - 'validate-number' => true, - 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE, - ], - 'imports' => [ - 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal', - ], - 'sortOrder' => 10, - 'disabled' => $this->locator->getProduct()->isLockedAttribute('quantity_and_stock_status'), - ] - ] - ] - ]; - } - - /** - * Get advances inventory button meta structure - * - * @return array - */ - private function getAdvancedInventoryButtonMetaStructure() - { - return [ - 'arguments' => [ - 'data' => [ - 'config' => [ - 'displayAsLink' => true, - 'formElement' => 'container', - 'componentType' => 'container', - 'component' => 'Magento_Ui/js/form/components/button', - 'template' => 'ui/form/components/button/container', - 'actions' => [ - [ - 'targetName' => 'product_form.product_form.advanced_inventory_modal', - 'actionName' => 'toggleModal', - ], - ], - 'title' => __('Advanced Inventory'), - 'provider' => false, - 'additionalForGroup' => true, - 'source' => 'product_details', - 'sortOrder' => 20, - ] - ] - ] - ]; - } } diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml index 08ed0a8f49470..546f838b9b428 100644 --- a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml @@ -55,7 +55,7 @@ </field> <field id="max_sale_qty" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Maximum Qty Allowed in Shopping Cart</label> - <validate>validate-number</validate> + <validate>validate-number validate-greater-than-zero</validate> </field> <field id="min_qty" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Out-of-Stock Threshold</label> diff --git a/app/code/Magento/CatalogInventory/etc/db_schema.xml b/app/code/Magento/CatalogInventory/etc/db_schema.xml index 5ac7fedc5aa18..b5c4a96f24a94 100644 --- a/app/code/Magento/CatalogInventory/etc/db_schema.xml +++ b/app/code/Magento/CatalogInventory/etc/db_schema.xml @@ -9,9 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="cataloginventory_stock" resource="default" engine="innodb" comment="Cataloginventory Stock"> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="varchar" name="stock_name" nullable="true" length="255" comment="Stock Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="stock_id"/> @@ -22,11 +22,11 @@ </table> <table name="cataloginventory_stock_item" resource="default" engine="innodb" comment="Cataloginventory Stock Item"> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Stock Id"/> + default="0" comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Qty"/> <column xsi:type="decimal" name="min_qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Min Qty"/> @@ -94,11 +94,11 @@ <table name="cataloginventory_stock_status" resource="default" engine="innodb" comment="Cataloginventory Stock Status"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" @@ -121,11 +121,11 @@ <table name="cataloginventory_stock_status_idx" resource="default" engine="innodb" comment="Cataloginventory Stock Status Indexer Idx"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" @@ -142,14 +142,14 @@ <column name="website_id"/> </index> </table> - <table name="cataloginventory_stock_status_tmp" resource="default" engine="memory" + <table name="cataloginventory_stock_status_tmp" resource="default" engine="innodb" comment="Cataloginventory Stock Status Indexer Tmp"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" @@ -159,21 +159,21 @@ <column name="website_id"/> <column name="stock_id"/> </constraint> - <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="hash"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> - <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="hash"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="cataloginventory_stock_status_replica" resource="default" engine="innodb" comment="Cataloginventory Stock Status"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index fc0690157fb37..c77c77a5183d0 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -35,9 +35,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Manage Stock</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> </item> </argument> <field name="manage_stock" formElement="select"> @@ -74,12 +72,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.manage_stock</link> </links> <exports> - <link name="disabled">${$.parentName}.manage_stock:disabled</link> <link name="checked">${$.parentName}.manage_stock:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -105,7 +99,6 @@ <dataScope>quantity_and_stock_status.qty</dataScope> <links> <link name="value">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:value</link> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> </links> <imports> <link name="handleChanges">${$.provider}:data.product.stock_data.is_qty_decimal</link> @@ -117,9 +110,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Out-of-Stock Threshold</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">${$.provider}:data.product.stock_data.manage_stock</item> </item> @@ -154,12 +145,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.min_qty</link> </links> <exports> - <link name="disabled">${$.parentName}.min_qty:disabled</link> <link name="checked">${$.parentName}.min_qty:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -220,13 +207,6 @@ <class name="admin__field-no-label">true</class> </additionalClasses> <dataScope>use_config_min_sale_qty</dataScope> - <exports> - <link name="disabled">${$.parentName}.min_sale_qty:disabled</link> - <link name="checked">${$.parentName}.min_sale_qty:disabled</link> - </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -290,9 +270,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Maximum Qty Allowed in Shopping Cart</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> </item> </argument> <field name="max_sale_qty" formElement="input"> @@ -304,6 +282,7 @@ <settings> <scopeLabel>[GLOBAL]</scopeLabel> <validation> + <rule name="validate-number" xsi:type="boolean">true</rule> <rule name="validate-greater-than-zero" xsi:type="boolean">true</rule> </validation> <label translate="true">Maximum Qty Allowed in Shopping Cart</label> @@ -324,12 +303,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.max_sale_qty</link> </links> <exports> - <link name="disabled">${$.parentName}.max_sale_qty:disabled</link> <link name="checked">${$.parentName}.max_sale_qty:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -357,7 +332,6 @@ <dataScope>stock_data.is_qty_decimal</dataScope> <imports> <link name="visible">${$.provider}:data.product.stock_data.manage_stock</link> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> </imports> </settings> <formElements> @@ -380,7 +354,6 @@ <dataScope>stock_data.is_decimal_divided</dataScope> <imports> <link name="visible">${$.provider}:data.product.stock_data.manage_stock</link> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> </imports> </settings> <formElements> @@ -395,9 +368,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Backorders</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">${$.provider}:data.product.stock_data.manage_stock</item> </item> @@ -440,12 +411,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.backorders</link> </links> <exports> - <link name="disabled">${$.parentName}.backorders:disabled</link> <link name="checked">${$.parentName}.backorders:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -464,9 +431,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Notify for Quantity Below</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">${$.provider}:data.product.stock_data.manage_stock</item> </item> @@ -501,12 +466,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.notify_stock_qty</link> </links> <exports> - <link name="disabled">${$.parentName}.notify_stock_qty:disabled</link> <link name="checked">${$.parentName}.notify_stock_qty:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -525,9 +486,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Enable Qty Increments</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> </item> </argument> <field name="enable_qty_increments" formElement="select"> @@ -564,12 +523,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.enable_qty_increments</link> </links> <exports> - <link name="disabled">${$.parentName}.enable_qty_increments:disabled</link> <link name="checked">${$.parentName}.enable_qty_increments:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -588,9 +543,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Qty Increments</item> <item name="dataScope" xsi:type="string">stock_data</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">${$.provider}:data.product.stock_data.enable_qty_increments</item> </item> @@ -629,12 +582,8 @@ <link name="linkedValue">${$.provider}:data.product.stock_data.qty_increments</link> </links> <exports> - <link name="disabled">${$.parentName}.qty_increments:disabled</link> <link name="checked">${$.parentName}.qty_increments:disabled</link> </exports> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <checkbox class="Magento\CatalogInventory\Ui\Component\Product\Form\Element\UseConfigSettings"> @@ -653,9 +602,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="formElement" xsi:type="string">container</item> - <item name="label" xsi:type="string" translate="true">Stock Status</item> <item name="dataScope" xsi:type="string">quantity_and_stock_status</item> - <item name="scopeLabel" xsi:type="string">[GLOBAL]</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">${$.provider}:data.product.stock_data.manage_stock</item> </item> @@ -671,9 +618,6 @@ <scopeLabel>[GLOBAL]</scopeLabel> <label translate="true">Stock Status</label> <dataScope>is_in_stock</dataScope> - <imports> - <link name="disabled">ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled</link> - </imports> </settings> <formElements> <select> diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php index 4f58293d53359..6d499b93e411f 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php @@ -12,6 +12,7 @@ use Magento\Framework\Registry; use Magento\Framework\Stdlib\DateTime\Filter\Date; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Save action for catalog rule @@ -25,19 +26,27 @@ class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog imple */ protected $dataPersistor; + /** + * @var TimezoneInterface + */ + private $localeDate; + /** * @param Context $context * @param Registry $coreRegistry * @param Date $dateFilter * @param DataPersistorInterface $dataPersistor + * @param TimezoneInterface $localeDate */ public function __construct( Context $context, Registry $coreRegistry, Date $dateFilter, - DataPersistorInterface $dataPersistor + DataPersistorInterface $dataPersistor, + TimezoneInterface $localeDate ) { $this->dataPersistor = $dataPersistor; + $this->localeDate = $localeDate; parent::__construct($context, $coreRegistry, $dateFilter); } @@ -46,16 +55,15 @@ public function __construct( * * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { if ($this->getRequest()->getPostValue()) { - /** @var \Magento\CatalogRule\Api\CatalogRuleRepositoryInterface $ruleRepository */ $ruleRepository = $this->_objectManager->get( \Magento\CatalogRule\Api\CatalogRuleRepositoryInterface::class ); - /** @var \Magento\CatalogRule\Model\Rule $model */ $model = $this->_objectManager->create(\Magento\CatalogRule\Model\Rule::class); @@ -65,7 +73,9 @@ public function execute() ['request' => $this->getRequest()] ); $data = $this->getRequest()->getPostValue(); - + if (!$this->getRequest()->getParam('from_date')) { + $data['from_date'] = $this->localeDate->formatDate(); + } $filterValues = ['from_date' => $this->_dateFilter]; if ($this->getRequest()->getParam('to_date')) { $filterValues['to_date'] = $this->_dateFilter; diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index e12eabba76401..1fc53c78985fb 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -242,7 +242,22 @@ public function __construct( */ public function reindexById($id) { - $this->reindexByIds([$id]); + try { + $this->cleanProductIndex([$id]); + + $products = $this->productLoader->getProducts([$id]); + $activeRules = $this->getActiveRules(); + foreach ($products as $product) { + $this->applyRules($activeRules, $product); + } + + $this->reindexRuleGroupWebsite->execute(); + } catch (\Exception $e) { + $this->critical($e); + throw new \Magento\Framework\Exception\LocalizedException( + __('Catalog rule indexing failed. See details in exception log.') + ); + } } /** @@ -275,11 +290,18 @@ protected function doReindexByIds($ids) { $this->cleanProductIndex($ids); - $products = $this->productLoader->getProducts($ids); - $activeRules = $this->getActiveRules(); - foreach ($products as $product) { - $this->applyRules($activeRules, $product); + /** @var Rule[] $activeRules */ + $activeRules = $this->getActiveRules()->getItems(); + foreach ($activeRules as $rule) { + $rule->setProductsFilter($ids); + $this->reindexRuleProduct->execute($rule, $this->batchCount); } + + foreach ($ids as $productId) { + $this->cleanProductPriceIndex([$productId]); + $this->reindexRuleProductPrice->execute($this->batchCount, $productId); + } + $this->reindexRuleGroupWebsite->execute(); } @@ -365,17 +387,13 @@ protected function cleanByIds($productIds) * Assign product to rule * * @param Rule $rule - * @param Product $product + * @param int $productEntityId + * @param array $websiteIds * @return void */ - private function assignProductToRule(Rule $rule, Product $product): void + private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void { - if (!$rule->validate($product)) { - return; - } - $ruleId = (int) $rule->getId(); - $productEntityId = (int) $product->getId(); $ruleProductTable = $this->getTable('catalogrule_product'); $this->connection->delete( $ruleProductTable, @@ -385,7 +403,6 @@ private function assignProductToRule(Rule $rule, Product $product): void ] ); - $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); $customerGroupIds = $rule->getCustomerGroupIds(); $fromTime = strtotime($rule->getFromDate()); $toTime = strtotime($rule->getToDate()); @@ -429,12 +446,17 @@ private function assignProductToRule(Rule $rule, Product $product): void * @param Product $product * @return $this * @throws \Exception + * @deprecated + * @see ReindexRuleProduct::execute * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function applyRule(Rule $rule, $product) { - $this->assignProductToRule($rule, $product); - $this->reindexRuleProductPrice->execute($this->batchCount, $product); + if ($rule->validate($product)) { + $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); + $this->assignProductToRule($rule, $product->getId(), $websiteIds); + } + $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId()); $this->reindexRuleGroupWebsite->execute(); return $this; @@ -450,11 +472,16 @@ protected function applyRule(Rule $rule, $product) private function applyRules(RuleCollection $ruleCollection, Product $product): void { foreach ($ruleCollection as $rule) { - $this->assignProductToRule($rule, $product); + if (!$rule->validate($product)) { + continue; + } + + $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); + $this->assignProductToRule($rule, $product->getId(), $websiteIds); } $this->cleanProductPriceIndex([$product->getId()]); - $this->reindexRuleProductPrice->execute($this->batchCount, $product); + $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId()); } /** @@ -507,7 +534,7 @@ protected function updateRuleProductData(Rule $rule) */ protected function applyAllRules(Product $product = null) { - $this->reindexRuleProductPrice->execute($this->batchCount, $product); + $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId()); $this->reindexRuleGroupWebsite->execute(); return $this; } @@ -562,7 +589,7 @@ protected function calcRuleProductPrice($ruleData, $productData = null) */ protected function getRuleProductsStmt($websiteId, Product $product = null) { - return $this->ruleProductsSelectBuilder->build($websiteId, $product); + return $this->ruleProductsSelectBuilder->build((int) $websiteId, (int) $product->getId()); } /** diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php index e589c8595ce2c..944710773123f 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php @@ -101,7 +101,9 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false) $scopeTz = new \DateTimeZone( $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) ); - $fromTime = (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp(); + $fromTime = $rule->getFromDate() + ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp() + : 0; $toTime = $rule->getToDate() ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 : 0; diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php index 11ba87730bec1..51869f1accbb3 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php @@ -6,7 +6,6 @@ namespace Magento\CatalogRule\Model\Indexer; -use Magento\Catalog\Model\Product; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Store\Model\StoreManagerInterface; @@ -65,19 +64,19 @@ public function __construct( * Reindex product prices. * * @param int $batchCount - * @param Product|null $product + * @param int|null $productId * @param bool $useAdditionalTable * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function execute($batchCount, Product $product = null, $useAdditionalTable = false) + public function execute(int $batchCount, ?int $productId = null, bool $useAdditionalTable = false) { /** * Update products rules prices per each website separately * because for each website date in website's timezone should be used */ foreach ($this->storeManager->getWebsites() as $website) { - $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $product, $useAdditionalTable); + $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $productId, $useAdditionalTable); $dayPrices = []; $stopFlags = []; $prevKey = null; diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php index 6989a33535ad8..e15bf6b3b1faa 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php @@ -74,15 +74,12 @@ public function __construct( * Build select for indexer according passed parameters. * * @param int $websiteId - * @param \Magento\Catalog\Model\Product|null $product + * @param int|null $productId * @param bool $useAdditionalTable * @return \Zend_Db_Statement_Interface */ - public function build( - $websiteId, - \Magento\Catalog\Model\Product $product = null, - $useAdditionalTable = false - ) { + public function build(int $websiteId, ?int $productId = null, bool $useAdditionalTable = false) + { $connection = $this->resource->getConnection(); $indexTable = $this->resource->getTableName('catalogrule_product'); if ($useAdditionalTable) { @@ -107,8 +104,8 @@ public function build( ['rp.website_id', 'rp.customer_group_id', 'rp.product_id', 'rp.sort_order', 'rp.rule_id'] ); - if ($product && $product->getEntityId()) { - $select->where('rp.product_id=?', $product->getEntityId()); + if ($productId) { + $select->where('rp.product_id=?', $productId); } /** @@ -159,9 +156,11 @@ public function build( sprintf($joinCondition, $tableAlias, $storeId), [] ); - $select->columns([ - 'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'), - ]); + $select->columns( + [ + 'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'), + ] + ); return $connection->query($select); } diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php index 7cbbc547571ab..ec63d70d55abf 100644 --- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php @@ -85,7 +85,8 @@ public function getValue() { if (null === $this->value) { if ($this->product->hasData(self::PRICE_CODE)) { - $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false; + $value = $this->product->getData(self::PRICE_CODE); + $this->value = $value ? (float)$value : false; } else { $this->value = $this->ruleResource->getRulePrice( $this->dateTime->scopeDate($this->storeManager->getStore()->getId()), diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml index 00dcb68089b73..209095e0b0195 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml @@ -21,7 +21,6 @@ <waitForPageLoad stepKey="waitForPageToLoad"/> <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> <fillField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> - <selectOption selector="{{AdminNewCatalogPriceRule.status}}" userInput="{{catalogRule.is_active}}" stepKey="selectStatus"/> <selectOption stepKey="selectWebSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}"/> <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/> <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml index 77fe0f50653c7..0a4b6366d11a8 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml @@ -13,7 +13,7 @@ <description>Validates that the provided Catalog Rule, Status, Websites and Customer Group details are present and correct on a Admin Catalog Price Rule creation/edit page.</description> </annotations> <arguments> - <argument name="catalogRule" defaultValue="inactiveCatalogRule"/> + <argument name="catalogRule" defaultValue="inactiveCatalogRule" /> <argument name="status" type="string" defaultValue=""/> <argument name="websites" type="string"/> <argument name="customerGroup" type="string"/> @@ -21,7 +21,6 @@ <seeInField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> <seeInField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> - <seeOptionIsSelected selector="{{AdminNewCatalogPriceRule.status}}" userInput="{{status}}" stepKey="selectStatus"/> <see stepKey="seeWebSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{websites}}"/> <seeOptionIsSelected selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/> <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index a7500858fc94e..50b4165a3f34b 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -25,6 +25,7 @@ <!-- Fill the form according the attributes of the entity --> <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> <fillField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <selectOption stepKey="selectSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}"/> <click stepKey="clickFromCalender" selector="{{AdminNewCatalogPriceRule.fromDateButton}}"/> <click stepKey="clickFromToday" selector="{{AdminNewCatalogPriceRule.todayDate}}"/> @@ -47,17 +48,39 @@ <arguments> <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> </arguments> - <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> - <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName"/> - <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription"/> - <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite"/> + <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName" /> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> + <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription" /> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite" /> <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> <waitForPageLoad stepKey="waitForApplied"/> </actionGroup> + <actionGroup name="AdminCreateCatalogPriceRuleWithConditionActionGroup" extends="createCatalogPriceRule"> + <arguments> + <argument name="catalogRuleType" type="entity" defaultValue="PriceRuleWithCondition"/> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad" after="addNewRule"/> + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="expandConditions" before="openActionDropdown"/> + <scrollTo selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="scrollToConditionsTab" after="expandConditions"/> + <waitForElementVisible selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="waitForNewRule" after="scrollToConditionsTab"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule" after="waitForNewRule"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionsDropdown}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="selectProductAttribute" after="clickNewRule"/> + <waitForPageLoad stepKey="waitForAttributeLoad" after="selectProductAttribute"/> + <!--Assert that attribute contains today date without time--> + <comment userInput="Assert that attribute contains today date without time" stepKey="assertDate" after="waitForAttributeLoad"/> + <generateDate date="now" format="Y-m-d" stepKey="today" after="assertDate"/> + <grabTextFrom selector="{{PriceRuleConditionsSection.firstProductAttributeSelected}}" stepKey="grabTextFromSelectedAttribute" after="today"/> + <assertEquals expected="$today" actual="$grabTextFromSelectedAttribute" stepKey="assertTodayDate" after="grabTextFromSelectedAttribute"/> + </actionGroup> + <actionGroup name="AdminCreateMultipleWebsiteCatalogPriceRule" extends="createCatalogPriceRule"> + <remove keyForRemoval="selectSite"/> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="['FirstWebsite', 'SecondWebsite']" stepKey="selectWebsite"/> + </actionGroup> <actionGroup name="CreateCatalogPriceRuleViaTheUi"> <arguments> <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> @@ -185,4 +208,48 @@ <waitForElementVisible selector="{{AdminMessagesSection.success}}" after="clickToConfirm" stepKey="waitForSuccessMessage"/> <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." after="waitForSuccessMessage" stepKey="verifyRuleIsDeleted"/> </actionGroup> + + <actionGroup name="AdminFillCatalogRuleConditionActionGroup"> + <annotations> + <description>Clicks on the Conditions tab. Fills in the provided condition for Catalog Price Rule.</description> + </annotations> + <arguments> + <argument name="condition" type="string" defaultValue="{{CatalogRuleProductConditions.categoryIds}}"/> + <argument name="conditionOperator" type="string" defaultValue="is"/> + <argument name="conditionValue" type="string" defaultValue="2"/> + </arguments> + + <conditionalClick selector="{{AdminNewCatalogPriceRule.conditionsTab}}" dependentSelector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" visible="false" stepKey="openConditionsTab"/> + <waitForElementVisible selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="waitForAddConditionButton"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="addNewCondition"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="{{condition}}" stepKey="selectTypeCondition"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.condition('is')}}" stepKey="clickOnOperator"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.activeOperatorSelect}}" userInput="{{conditionOperator}}" stepKey="selectCondition"/> + <!-- In case we are choosing already selected value - select is not closed automatically --> + <conditionalClick selector="{{AdminNewCatalogPriceRuleConditions.condition('...')}}" dependentSelector="{{AdminNewCatalogPriceRuleConditions.activeOperatorSelect}}" visible="true" stepKey="closeSelect"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.condition('...')}}" stepKey="clickToChooseOption3"/> + <waitForElementVisible selector="{{AdminNewCatalogPriceRuleConditions.activeValueInput}}" stepKey="waitForValueInput"/> + <fillField selector="{{AdminNewCatalogPriceRuleConditions.activeValueInput}}" userInput="{{conditionValue}}" stepKey="fillConditionValue"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.activeConditionApplyButton}}" stepKey="clickApply"/> + <waitForElementNotVisible selector="{{AdminNewCatalogPriceRuleConditions.activeConditionApplyButton}}" stepKey="waitForApplyButtonInvisibility"/> + </actionGroup> + + <actionGroup name="newCatalogPriceRuleWithInvalidData"> + <annotations> + <description>Goes to the Catalog Price Rule grid. Clicks on Add. Fills in the provided Catalog Rule details with invalid data.</description> + </annotations> + <arguments> + <argument name="catalogRule" defaultValue="catalogRuleWithInvalid"/> + </arguments> + + <!-- Go to the admin Catalog rule grid and add a new one --> + <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <waitForPageLoad stepKey="waitForPriceRulePage"/> + + <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> + <fillField stepKey="fillPriority" selector="{{AdminNewCatalogPriceRule.priority}}" userInput="{{catalogRule.priority}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForApplied"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml index 75a7484324576..2920a895f607d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -173,4 +173,19 @@ <data key="defaultRuleLabelAllStoreViews">Free Shipping in conditions</data> <data key="defaultStoreView">Free Shipping in conditions</data> </entity> + + <entity name="catalogRuleWithInvalid" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + <data key="priority">ten</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml new file mode 100644 index 0000000000000..510ea25c3f566 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.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="CatalogRuleProductConditions"> + <data key="categoryIds">Magento\CatalogRule\Model\Rule\Condition\Product|category_ids</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml index 0d89c7970b852..5b6b610508fef 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml @@ -9,8 +9,9 @@ <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="createCatalogRule" dataType="catalogRule" - type="create" auth="adminFormKey" url="/catalog_rule/promo_catalog/save/" method="POST"> + <operation name="createCatalogRule" dataType="catalogRule" type="create" auth="adminFormKey" + url="/catalog_rule/promo_catalog/save/back/edit" method="POST" + returnRegex="~\/id\/(?'id'\d+)\/~" returnIndex="id" successRegex="/messages-message-success/"> <contentType>application/x-www-form-urlencoded</contentType> <field key="name">string</field> <field key="description">string</field> @@ -24,4 +25,7 @@ <field key="simple_action">string</field> <field key="discount_amount">string</field> </operation> + <operation name="DeleteCatalogRule" dataType="catalogRule" type="delete" auth="adminFormKey" + url="/catalog_rule/promo_catalog/delete/id/{return}" method="POST" successRegex="/messages-message-success/"> + </operation> </operations> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml index fded4f5e5f322..71372481490e8 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminNewCatalogPriceRulePage" url="catalog_rule/promo_catalog/new/" module="Magento_CatalogRule" area="admin"> <section name="AdminNewCatalogPriceRule"/> + <section name="AdminNewCatalogPriceRuleConditions"/> </page> </pages> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml index ba0493d8e995b..7d375da6dfb65 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml @@ -20,8 +20,12 @@ <element name="ruleNameNew" type="input" selector="[name='staging[name]']"/> <element name="description" type="textarea" selector="[name='description']"/> <element name="status" type="select" selector="[name='is_active']"/> + <element name="isActive" type="select" selector="input[name='is_active']+label"/> <element name="websites" type="select" selector="[name='website_ids']"/> + <element name="active" type="checkbox" selector="//div[contains(@class, 'admin__actions-switch')]/input[@name='is_active']/../label"/> + <element name="activeIsEnabled" type="checkbox" selector="(//div[contains(@class, 'admin__actions-switch')])[1]/input[@value='1']"/> + <element name="activePosition" type="checkbox" selector="fieldset[class='admin__fieldset'] div[class*='_required']:nth-of-type(4)"/> <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> <element name="customerGroups" type="select" selector="[name='customer_group_ids']"/> <element name="customerGroupsOptions" type="select" selector="[name='customer_group_ids'] option"/> @@ -33,6 +37,8 @@ <element name="priority" type="input" selector="[name='sort_order']"/> <element name="conditionsTab" type="block" selector="[data-index='block_promo_catalog_edit_tab_conditions']"/> <element name="actionsTab" type="block" selector="[data-index='actions']"/> + + <element name="fieldError" type="text" selector="//input[@name='{{fieldName}}']/following-sibling::label[@class='admin__field-error']" parameterized="true"/> </section> <section name="AdminNewCatalogPriceRuleActions"> @@ -42,8 +48,9 @@ </section> <section name="AdminNewCatalogPriceRuleConditions"> - <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child"/> - <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true"/> + <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child" timeout="30"/> + <element name="conditionsDropdown" type="select" selector="select[data-form-part='catalog_rule_form'][data-ui-id='newchild-0-select-rule-conditions-1-new-child']"/> + <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true" timeout="30"/> <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true"/> <element name="targetEllipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '{{var1}}')]" parameterized="true" timeout="30"/> <element name="ellipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '...')]" parameterized="true" timeout="30"/> @@ -51,6 +58,10 @@ <element name="targetSelect" type="select" selector="//ul[@id='conditions__{{var}}__children']//select" parameterized="true" timeout="30"/> <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true"/> + <element name="condition" type="text" selector="//span[@class='rule-param']/a[text()='{{condition}}']" parameterized="true"/> + <element name="activeOperatorSelect" type="select" selector=".rule-param-edit select[name*='[operator]']"/> + <element name="activeValueInput" type="input" selector=".rule-param-edit [name*='[value]']"/> + <element name="activeConditionApplyButton" type="button" selector=".rule-param-edit .rule-param-apply" timeout="30"/> </section> <section name="AdminCatalogPriceRuleGrid"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml index 741da96179b8c..ca534ec7f5375 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml @@ -52,6 +52,7 @@ <waitForPageLoad stepKey="waitForIndividualRulePage"/> <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{_defaultCatalogRule.name}}" stepKey="fillName"/> <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{_defaultCatalogRule.description}}" stepKey="fillDescription"/> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{_defaultCatalogRule.website_ids[0]}}" stepKey="selectSite"/> <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml index befe0b0ce7f98..ee61af180d350 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml @@ -25,6 +25,10 @@ <requiredEntity createDataKey="createCategory"/> </createData> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- log in and create the price rule --> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"/> @@ -150,6 +154,7 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> @@ -172,6 +177,10 @@ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- As a NOT LOGGED IN user, go to the storefront category page and should see the discount --> <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory1"/> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct1"/> @@ -187,4 +196,27 @@ <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct2"/> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$123.00" stepKey="seeDiscountedPrice2"/> </test> + + <test name="AdminCreateCatalogPriceRuleWithInvalidDataTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create Catalog Price Rule"/> + <title value="Admin can not create catalog price rule with the invalid data"/> + <description value="Admin can not create catalog price rule with the invalid data"/> + <severity value="MAJOR"/> + <group value="CatalogRule"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="newCatalogPriceRuleWithInvalidData" stepKey="createNewPriceRule"> + <argument name="catalogRule" value="catalogRuleWithInvalid"/> + </actionGroup> + + <see selector="{{AdminNewCatalogPriceRule.fieldError('sort_order')}}" userInput="Please enter a valid number in this field." stepKey="seeSortOrderError"/> + </test> </tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml index 5223b18df4e4a..83dff1ecdcab5 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml @@ -58,6 +58,7 @@ <argument name="websites" value="Main Website"/> <argument name="customerGroup" value="General"/> </actionGroup> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveRule"/> <!-- Search Catalog Rule in Grid --> <actionGroup ref="AdminSearchCatalogRuleInGridActionGroup" stepKey="searchCreatedCatalogRule"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml index 06392764290ac..d80759531ecae 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml @@ -18,7 +18,7 @@ <group value="CatalogRule"/> <group value="mtf_migrated"/> </annotations> - + <before> <createData entity="Simple_US_Customer" stepKey="createCustomer1"/> <createData entity="_defaultCategory" stepKey="createCategory1"/> @@ -59,7 +59,7 @@ <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> <!-- Assert that the Success message is present after the delete --> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> <!-- Reindex --> <magentoCLI command="cache:flush" stepKey="flushCache1"/> @@ -154,6 +154,10 @@ <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminNewCatalogPriceRulePage.url}}" stepKey="openNewCatalogPriceRulePage"/> @@ -188,7 +192,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> <!-- Reindex --> <magentoCLI command="cache:flush" stepKey="flushCache1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml index d3546d06492be..16fbca2697702 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml @@ -19,14 +19,14 @@ <group value="CatalogRule"/> </annotations> <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create a category --> <createData entity="ApiCategory" stepKey="createCategory"/> - <!-- Create a simple product --> <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - + <!-- Login to Admin page --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <!-- Create a configurable product --> <actionGroup ref="createConfigurableProduct" stepKey="createConfigurableProduct"> <argument name="product" value="_defaultProduct"/> @@ -34,9 +34,16 @@ </actionGroup> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToCatalogRuleGridPage"/> + <waitForPageLoad stepKey="waitForCatalogRuleGridPageLoaded"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCatalogRuleGridFilters"/> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> </after> <!-- Create a catalog price rule --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml index b7a231df5045d..b9d601238ac73 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml @@ -16,6 +16,9 @@ <severity value="CRITICAL"/> <testCaseId value="MC-148"/> <group value="CatalogRule"/> + <skip> + <issueId value="MC-22577"/> + </skip> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="login"/> @@ -148,7 +151,7 @@ <argument name="attribute" value="{{productAttributeDropdownTwoOptions.attribute_code}}"/> </actionGroup> <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> - + <!-- First Simple Product: choose green as attribute value --> <actionGroup ref="filterAndSelectProduct" stepKey="openFirstSimpleProduct"> <argument name="productSku" value="$$createFirstProduct.sku$$"/> @@ -195,6 +198,7 @@ Websites: Main Website Customer Groups: NOT LOGGED IN --> <fillField userInput="{{SimpleCatalogPriceRule.name}}" selector="{{AdminCartPriceRulesFormSection.ruleName}}" stepKey="fillRuleName"/> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <selectOption userInput="{{SimpleCatalogPriceRule.websites}}" selector="{{AdminCartPriceRulesFormSection.websites}}" stepKey="selectWebsite"/> <selectOption userInput="{{SimpleCatalogPriceRule.customerGroups}}" selector="{{AdminCartPriceRulesFormSection.customerGroups}}" stepKey="selectCustomerGroups"/> @@ -228,6 +232,7 @@ <!-- Run cron twice --> <magentoCLI command="cron:run" stepKey="runCron1"/> <magentoCLI command="cron:run" stepKey="runCron2"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!-- Go to Frontend and open the simple product --> <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.sku$$)}}" stepKey="amOnSimpleProductPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 5b7e722c92a02..a251ee1e235d0 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -77,63 +77,27 @@ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <magentoCLI command="indexer:reindex" stepKey="reindex"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> - + <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> - - <!-- Check product 1 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> - <argument name="productInfo" value="$createProduct1.name$"/> - <argument name="productNumber" value="3"/> - </actionGroup> <!-- Check product 1 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> - <argument name="productInfo" value="$51.10"/> - <argument name="productNumber" value="3"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$51.10" stepKey="storefrontProduct1Price"/> <!-- Check product 1 regular price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> - <argument name="productInfo" value="$56.78"/> - <argument name="productNumber" value="3"/> - </actionGroup> - - <!-- Check product 2 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Name"> - <argument name="productInfo" value="$createProduct2.name$"/> - <argument name="productNumber" value="2"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$56.78" stepKey="storefrontProduct1RegularPrice"/> <!-- Check product 2 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Price"> - <argument name="productInfo" value="$51.10"/> - <argument name="productNumber" value="2"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$51.10" stepKey="storefrontProduct2Price"/> - <!-- Check product 2 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2RegularPrice"> - <argument name="productInfo" value="$56.78"/> - <argument name="productNumber" value="2"/> - </actionGroup> - - <!-- Check product 3 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3Name"> - <argument name="productInfo" value="$createProduct3.name$"/> - <argument name="productNumber" value="1"/> - </actionGroup> + <!-- Check product 2 regular price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$56.78" stepKey="storefrontProduct2RegularPrice"/> <!-- Check product 3 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3Price"> - <argument name="productInfo" value="$51.10"/> - <argument name="productNumber" value="1"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$51.10" stepKey="storefrontProduct3Price"/> <!-- Check product 3 regular price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3RegularPrice"> - <argument name="productInfo" value="$56.78"/> - <argument name="productNumber" value="1"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$56.78" stepKey="storefrontProduct3RegularPrice"/> <!-- Navigate to product 1 on store front --> <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index b486654fe9acf..08e59c6316411 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -26,9 +26,14 @@ </createData> <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"/> <actionGroup stepKey="selectLoggedInCustomers" ref="selectNotLoggedInCustomerGroup"/> - <selectOption selector="{{AdminNewCatalogPriceRule.status}}" userInput="Inactive" stepKey="setInactive"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="setInactive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="seeSuccess"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php deleted file mode 100644 index 78668366bccdc..0000000000000 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php +++ /dev/null @@ -1,289 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogRule\Test\Unit\Model\Indexer; - -use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.TooManyFields) - */ -class IndexBuilderTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\CatalogRule\Model\Indexer\IndexBuilder - */ - protected $indexBuilder; - - /** - * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resource; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeManager; - - /** - * @var \Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $ruleCollectionFactory; - - /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $logger; - - /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $priceCurrency; - - /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ - protected $eavConfig; - - /** - * @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject - */ - protected $dateFormat; - - /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime|\PHPUnit_Framework_MockObject_MockObject - */ - protected $dateTime; - - /** - * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productFactory; - - /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $connection; - - /** - * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject - */ - protected $metadataPool; - - /** - * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject - */ - protected $select; - - /** - * @var \Zend_Db_Statement_Interface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $db; - - /** - * @var \Magento\Store\Model\Website|\PHPUnit_Framework_MockObject_MockObject - */ - protected $website; - - /** - * @var \Magento\Rule\Model\Condition\Combine|\PHPUnit_Framework_MockObject_MockObject - */ - protected $combine; - - /** - * @var \Magento\CatalogRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject - */ - protected $rules; - - /** - * @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject - */ - protected $product; - - /** - * @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject - */ - protected $attribute; - - /** - * @var \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend|\PHPUnit_Framework_MockObject_MockObject - */ - protected $backend; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reindexRuleProductPrice; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reindexRuleGroupWebsite; - - /** - * @var ProductLoader|\PHPUnit_Framework_MockObject_MockObject - */ - private $productLoader; - - /** - * Set up test - * - * @return void - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp() - { - $this->resource = $this->createPartialMock( - \Magento\Framework\App\ResourceConnection::class, - ['getConnection', 'getTableName'] - ); - $this->ruleCollectionFactory = $this->createPartialMock( - \Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory::class, - ['create'] - ); - $this->backend = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class); - $this->select = $this->createMock(\Magento\Framework\DB\Select::class); - $this->metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); - $metadata = $this->createMock(\Magento\Framework\EntityManager\EntityMetadata::class); - $this->metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata); - $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $this->db = $this->createMock(\Zend_Db_Statement_Interface::class); - $this->website = $this->createMock(\Magento\Store\Model\Website::class); - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->combine = $this->createMock(\Magento\Rule\Model\Condition\Combine::class); - $this->rules = $this->createMock(\Magento\CatalogRule\Model\Rule::class); - $this->logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->attribute = $this->createMock(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class); - $this->priceCurrency = $this->createMock(\Magento\Framework\Pricing\PriceCurrencyInterface::class); - $this->dateFormat = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); - $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTime::class); - $this->eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getAttribute']); - $this->product = $this->createMock(\Magento\Catalog\Model\Product::class); - $this->productFactory = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create']); - $this->connection->expects($this->any())->method('select')->will($this->returnValue($this->select)); - $this->connection->expects($this->any())->method('query')->will($this->returnValue($this->db)); - $this->select->expects($this->any())->method('distinct')->will($this->returnSelf()); - $this->select->expects($this->any())->method('where')->will($this->returnSelf()); - $this->select->expects($this->any())->method('from')->will($this->returnSelf()); - $this->select->expects($this->any())->method('order')->will($this->returnSelf()); - $this->resource->expects($this->any())->method('getConnection')->will($this->returnValue($this->connection)); - $this->resource->expects($this->any())->method('getTableName')->will($this->returnArgument(0)); - $this->storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$this->website])); - $this->storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($this->website)); - $this->rules->expects($this->any())->method('getId')->will($this->returnValue(1)); - $this->rules->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1])); - $this->rules->expects($this->any())->method('getCustomerGroupIds')->will($this->returnValue([1])); - - $ruleCollection = $this->createMock(\Magento\CatalogRule\Model\ResourceModel\Rule\Collection::class); - $this->ruleCollectionFactory->expects($this->once()) - ->method('create') - ->willReturn($ruleCollection); - $ruleCollection->expects($this->once()) - ->method('addFieldToFilter') - ->willReturnSelf(); - $ruleIterator = new \ArrayIterator([$this->rules]); - $ruleCollection->method('getIterator') - ->willReturn($ruleIterator); - - $this->product->expects($this->any())->method('load')->will($this->returnSelf()); - $this->product->expects($this->any())->method('getId')->will($this->returnValue(1)); - $this->product->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1])); - - $this->rules->expects($this->any())->method('validate')->with($this->product)->willReturn(true); - $this->attribute->expects($this->any())->method('getBackend')->will($this->returnValue($this->backend)); - $this->productFactory->expects($this->any())->method('create')->will($this->returnValue($this->product)); - $this->productLoader = $this->getMockBuilder(ProductLoader::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->indexBuilder = (new ObjectManager($this))->getObject( - \Magento\CatalogRule\Model\Indexer\IndexBuilder::class, - [ - 'ruleCollectionFactory' => $this->ruleCollectionFactory, - 'priceCurrency' => $this->priceCurrency, - 'resource' => $this->resource, - 'storeManager' => $this->storeManager, - 'logger' => $this->logger, - 'eavConfig' => $this->eavConfig, - 'dateFormat' => $this->dateFormat, - 'dateTime' => $this->dateTime, - 'productFactory' => $this->productFactory, - 'productLoader' => $this->productLoader, - ] - ); - - $this->reindexRuleProductPrice = $this->createMock( - \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class - ); - $this->reindexRuleGroupWebsite = $this->createMock( - \Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class - ); - $this->setProperties( - $this->indexBuilder, - [ - 'metadataPool' => $this->metadataPool, - 'reindexRuleProductPrice' => $this->reindexRuleProductPrice, - 'reindexRuleGroupWebsite' => $this->reindexRuleGroupWebsite, - ] - ); - } - - /** - * Test UpdateCatalogRuleGroupWebsiteData - * - * @covers \Magento\CatalogRule\Model\Indexer\IndexBuilder::updateCatalogRuleGroupWebsiteData - * @return void - */ - public function testUpdateCatalogRuleGroupWebsiteData() - { - $priceAttrMock = $this->createPartialMock(\Magento\Catalog\Model\Entity\Attribute::class, ['getBackend']); - $backendModelMock = $this->createPartialMock( - \Magento\Catalog\Model\Product\Attribute\Backend\Tierprice::class, - ['getResource'] - ); - $resourceMock = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice::class, - ['getMainTable'] - ); - $resourceMock->expects($this->any()) - ->method('getMainTable') - ->will($this->returnValue('catalog_product_entity_tier_price')); - $backendModelMock->expects($this->any()) - ->method('getResource') - ->will($this->returnValue($resourceMock)); - $priceAttrMock->expects($this->any()) - ->method('getBackend') - ->will($this->returnValue($backendModelMock)); - - $iterator = [$this->product]; - $this->productLoader->expects($this->once()) - ->method('getProducts') - ->willReturn($iterator); - - $this->reindexRuleProductPrice->expects($this->once())->method('execute')->willReturn(true); - $this->reindexRuleGroupWebsite->expects($this->once())->method('execute')->willReturn(true); - - $this->indexBuilder->reindexByIds([1]); - } - - /** - * @param $object - * @param array $properties - */ - private function setProperties($object, $properties = []) - { - $reflectionClass = new \ReflectionClass(get_class($object)); - foreach ($properties as $key => $value) { - if ($reflectionClass->hasProperty($key)) { - $reflectionProperty = $reflectionClass->getProperty($key); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($object, $value); - } - } - } -} diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php index 5f63283df6760..d0f266d574945 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php @@ -71,6 +71,7 @@ public function testExecute() $websiteId = 234; $defaultGroupId = 11; $defaultStoreId = 22; + $productId = 55; $websiteMock = $this->createMock(WebsiteInterface::class); $websiteMock->expects($this->once()) @@ -93,11 +94,10 @@ public function testExecute() ->with($defaultGroupId) ->willReturn($groupMock); - $productMock = $this->createMock(Product::class); $statementMock = $this->createMock(\Zend_Db_Statement_Interface::class); $this->ruleProductsSelectBuilderMock->expects($this->once()) ->method('build') - ->with($websiteId, $productMock, true) + ->with($websiteId, $productId, true) ->willReturn($statementMock); $ruleData = [ @@ -126,6 +126,6 @@ public function testExecute() $this->pricesPersistorMock->expects($this->once()) ->method('execute'); - $this->assertTrue($this->model->execute(1, $productMock, true)); + $this->assertTrue($this->model->execute(1, $productId, true)); } } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php deleted file mode 100644 index e43fe41dc2127..0000000000000 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php +++ /dev/null @@ -1,200 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogRule\Test\Unit\Model\Indexer; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; -use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface; -use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Framework\DB\Select; -use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; -use Magento\Framework\EntityManager\EntityMetadataInterface; -use Magento\Store\Api\Data\WebsiteInterface; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class RuleProductsSelectBuilderTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder - */ - private $model; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $storeManagerMock; - - /** - * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject - */ - private $resourceMock; - - /** - * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject - */ - private $activeTableSwitcherMock; - - /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ - private $eavConfigMock; - - /** - * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject - */ - private $metadataPoolMock; - - /** - * @var IndexerTableSwapperInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $tableSwapperMock; - - protected function setUp() - { - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->getMockForAbstractClass(); - $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->activeTableSwitcherMock = $this->getMockBuilder(ActiveTableSwitcher::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) - ->disableOriginalConstructor() - ->getMock(); - $this->tableSwapperMock = $this->getMockForAbstractClass( - IndexerTableSwapperInterface::class - ); - - $this->model = new \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder( - $this->resourceMock, - $this->eavConfigMock, - $this->storeManagerMock, - $this->metadataPoolMock, - $this->activeTableSwitcherMock, - $this->tableSwapperMock - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testBuild() - { - $websiteId = 55; - $ruleTable = 'catalogrule_product'; - $rplTable = 'catalogrule_product_replica'; - $prTable = 'catalog_product_entity'; - $wsTable = 'catalog_product_website'; - $productMock = $this->getMockBuilder(Product::class)->disableOriginalConstructor()->getMock(); - $productMock->expects($this->exactly(2))->method('getEntityId')->willReturn(500); - - $connectionMock = $this->getMockBuilder(AdapterInterface::class)->disableOriginalConstructor()->getMock(); - $this->resourceMock->expects($this->at(0))->method('getConnection')->willReturn($connectionMock); - - $this->tableSwapperMock->expects($this->once()) - ->method('getWorkingTableName') - ->with($ruleTable) - ->willReturn($rplTable); - - $this->resourceMock->expects($this->at(1))->method('getTableName')->with($ruleTable)->willReturn($ruleTable); - $this->resourceMock->expects($this->at(2))->method('getTableName')->with($rplTable)->willReturn($rplTable); - $this->resourceMock->expects($this->at(3))->method('getTableName')->with($prTable)->willReturn($prTable); - $this->resourceMock->expects($this->at(4))->method('getTableName')->with($wsTable)->willReturn($wsTable); - - $selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); - $connectionMock->expects($this->once())->method('select')->willReturn($selectMock); - $selectMock->expects($this->at(0))->method('from')->with(['rp' => $rplTable])->willReturnSelf(); - $selectMock->expects($this->at(1)) - ->method('order') - ->with(['rp.website_id', 'rp.customer_group_id', 'rp.product_id', 'rp.sort_order', 'rp.rule_id']) - ->willReturnSelf(); - $selectMock->expects($this->at(2))->method('where')->with('rp.product_id=?', 500)->willReturnSelf(); - - $attributeMock = $this->getMockBuilder(AbstractAttribute::class)->disableOriginalConstructor()->getMock(); - $this->eavConfigMock->expects($this->once()) - ->method('getAttribute') - ->with(Product::ENTITY, 'price') - ->willReturn($attributeMock); - $backendMock = $this->getMockBuilder(AbstractBackend::class)->disableOriginalConstructor()->getMock(); - $backendMock->expects($this->once())->method('getTable')->willReturn('price_table'); - $attributeMock->expects($this->once())->method('getBackend')->willReturn($backendMock); - $attributeMock->expects($this->once())->method('getId')->willReturn(200); - - $metadataMock = $this->getMockBuilder(EntityMetadataInterface::class)->disableOriginalConstructor()->getMock(); - $this->metadataPoolMock->expects($this->once()) - ->method('getMetadata') - ->with(\Magento\Catalog\Api\Data\ProductInterface::class) - ->willReturn($metadataMock); - $metadataMock->expects($this->once())->method('getLinkField')->willReturn('link_field'); - - $selectMock->expects($this->at(3)) - ->method('join') - ->with(['e' => $prTable], 'e.entity_id = rp.product_id', []) - ->willReturnSelf(); - $selectMock->expects($this->at(4)) - ->method('join') - ->with( - ['pp_default' => 'price_table'], - 'pp_default.link_field=e.link_field AND (pp_default.attribute_id=200) and pp_default.store_id=0', - [] - )->willReturnSelf(); - $websiteMock = $this->getMockBuilder(WebsiteInterface::class) - ->setMethods(['getDefaultGroup']) - ->getMockForAbstractClass(); - $this->storeManagerMock->expects($this->once()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($websiteMock); - - $groupMock = $this->getMockBuilder(\Magento\Store\Model\Group::class) - ->setMethods(['getDefaultStoreId']) - ->disableOriginalConstructor() - ->getMock(); - $websiteMock->expects($this->once())->method('getDefaultGroup')->willReturn($groupMock); - $groupMock->expects($this->once())->method('getDefaultStoreId')->willReturn(700); - - $selectMock->expects($this->at(5)) - ->method('joinInner') - ->with( - ['product_website' => $wsTable], - 'product_website.product_id=rp.product_id ' - . 'AND product_website.website_id = rp.website_id ' - . 'AND product_website.website_id=' - . $websiteId, - [] - )->willReturnSelf(); - $selectMock->expects($this->at(6)) - ->method('joinLeft') - ->with( - ['pp' . $websiteId => 'price_table'], - 'pp55.link_field=e.link_field AND (pp55.attribute_id=200) and pp55.store_id=700', - [] - )->willReturnSelf(); - - $connectionMock->expects($this->once()) - ->method('getIfNullSql') - ->with('pp55.value', 'pp_default.value') - ->willReturn('IF NULL SQL'); - $selectMock->expects($this->at(7)) - ->method('columns') - ->with(['default_price' => 'IF NULL SQL']) - ->willReturnSelf(); - $statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class) - ->disableOriginalConstructor() - ->getMock(); - $connectionMock->expects($this->once())->method('query')->with($selectMock)->willReturn($statementMock); - - $this->assertEquals($statementMock, $this->model->build($websiteId, $productMock, true)); - } -} diff --git a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php index 7514d2bc4b5c5..54cd9e411df5c 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php @@ -176,4 +176,17 @@ public function testGetAmountNoBaseAmount() $result = $this->object->getValue(); $this->assertFalse($result); } + + public function testGetValueWithNullAmount() + { + $catalogRulePrice = null; + $convertedPrice = 0.0; + + $this->saleableItemMock->expects($this->once())->method('hasData') + ->with('catalog_rule_price')->willReturn(true); + $this->saleableItemMock->expects($this->once())->method('getData') + ->with('catalog_rule_price')->willReturn($catalogRulePrice); + + $this->assertEquals($convertedPrice, $this->object->getValue()); + } } diff --git a/app/code/Magento/CatalogRule/etc/db_schema.xml b/app/code/Magento/CatalogRule/etc/db_schema.xml index 59082e93b04c2..b3692f280fec5 100644 --- a/app/code/Magento/CatalogRule/etc/db_schema.xml +++ b/app/code/Magento/CatalogRule/etc/db_schema.xml @@ -37,7 +37,7 @@ </table> <table name="catalogrule_product" resource="default" engine="innodb" comment="CatalogRule Product"> <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Product Id"/> + comment="Rule Product ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" @@ -46,7 +46,7 @@ comment="To time"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="varchar" name="action_operator" nullable="true" length="10" default="to_fixed" comment="Action Operator"/> <column xsi:type="decimal" name="action_amount" scale="6" precision="20" unsigned="false" nullable="false" @@ -56,7 +56,7 @@ <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_id"/> </constraint> @@ -91,11 +91,11 @@ <column xsi:type="date" name="rule_date" nullable="false" comment="Rule Date"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="decimal" name="rule_price" scale="6" precision="20" unsigned="false" nullable="false" default="0" comment="Rule Price"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -121,9 +121,9 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group Id"/> + default="0" comment="Customer Group ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> @@ -140,7 +140,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> @@ -160,7 +160,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> @@ -177,7 +177,7 @@ </table> <table name="catalogrule_product_replica" resource="default" engine="innodb" comment="CatalogRule Product"> <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Product Id"/> + comment="Rule Product ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" @@ -186,7 +186,7 @@ comment="To time"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="varchar" name="action_operator" nullable="true" default="to_fixed" length="10" comment="Action Operator"/> <column xsi:type="decimal" name="action_amount" scale="6" precision="20" unsigned="false" nullable="false" @@ -196,7 +196,7 @@ <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_id"/> </constraint> @@ -232,11 +232,11 @@ <column xsi:type="date" name="rule_date" nullable="false" comment="Rule Date"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="decimal" name="rule_price" scale="6" precision="20" unsigned="false" nullable="false" default="0" comment="Rule Price"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -263,9 +263,9 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group Id"/> + default="0" comment="Customer Group ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> diff --git a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml index c114f6b1d77cd..59e3c4668e8a4 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml +++ b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml @@ -95,33 +95,30 @@ <dataScope>description</dataScope> </settings> </field> - <field name="is_active" formElement="select"> + <field name="is_active" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">catalog_rule</item> + <item name="default" xsi:type="number">0</item> </item> </argument> <settings> - <dataType>number</dataType> - <label translate="true">Status</label> - <visible>true</visible> - <dataScope>is_active</dataScope> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + <dataType>boolean</dataType> + <label translate="true">Active</label> </settings> <formElements> - <select> + <checkbox> <settings> - <options> - <option name="0" xsi:type="array"> - <item name="value" xsi:type="number">1</item> - <item name="label" xsi:type="string" translate="true">Active</item> - </option> - <option name="1" xsi:type="array"> - <item name="value" xsi:type="number">0</item> - <item name="label" xsi:type="string" translate="true">Inactive</item> - </option> - </options> + <valueMap> + <map name="false" xsi:type="number">0</map> + <map name="true" xsi:type="number">1</map> + </valueMap> + <prefer>toggle</prefer> </settings> - </select> + </checkbox> </formElements> </field> <field name="website_ids" formElement="multiselect"> @@ -211,6 +208,9 @@ </item> </argument> <settings> + <validation> + <rule name="validate-digits" xsi:type="boolean">true</rule> + </validation> <dataType>text</dataType> <label translate="true">Priority</label> <dataScope>sort_order</dataScope> diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index c758e773f43c1..a97d362c5de7f 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -165,7 +165,10 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu $this->customerSession->getCustomerGroupId() ); } elseif ($filter->getField() === 'category_ids') { - return 'category_ids_index.category_id = ' . (int) $filter->getValue(); + return $this->connection->quoteInto( + 'category_ids_index.category_id in (?)', + $filter->getValue() + ); } elseif ($attribute->isStatic()) { $alias = $this->aliasResolver->getAlias($filter); $resultQuery = str_replace( @@ -198,8 +201,9 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu ) ->joinLeft( ['current_store' => $table], - 'current_store.attribute_id = main_table.attribute_id AND current_store.store_id = ' - . $currentStoreId, + "current_store.{$linkIdField} = main_table.{$linkIdField} AND " + . "current_store.attribute_id = main_table.attribute_id AND current_store.store_id = " + . $currentStoreId, null ) ->columns([$filter->getField() => $ifNullCondition]) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 21d8b7297da7d..912dec8666191 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\Indexer; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory; +use Magento\CatalogSearch\Model\Indexer\Scope\State; use Magento\CatalogSearch\Model\Indexer\Scope\StateFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\DimensionProviderInterface; use Magento\Store\Model\StoreDimensionProvider; use Magento\Indexer\Model\ProcessManager; @@ -79,6 +82,7 @@ class Fulltext implements * @param DimensionProviderInterface $dimensionProvider * @param array $data * @param ProcessManager $processManager + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( FullFactory $fullActionFactory, @@ -95,11 +99,9 @@ public function __construct( $this->fulltextResource = $fulltextResource; $this->data = $data; $this->indexSwitcher = $indexSwitcher; - $this->indexScopeState = $indexScopeStateFactory->create(); + $this->indexScopeState = ObjectManager::getInstance()->get(State::class); $this->dimensionProvider = $dimensionProvider; - $this->processManager = $processManager ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - ProcessManager::class - ); + $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class); } /** @@ -127,9 +129,11 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = throw new \InvalidArgumentException('Indexer "' . self::INDEXER_ID . '" support only Store dimension'); } $storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue(); - $saveHandler = $this->indexerHandlerFactory->create([ - 'data' => $this->data - ]); + $saveHandler = $this->indexerHandlerFactory->create( + [ + 'data' => $this->data, + ] + ); if (null === $entityIds) { $this->indexScopeState->useTemporaryIndex(); diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 09d4f0068459a..cd2529a8fd725 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -7,14 +7,9 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; -use Magento\CatalogInventory\Api\Data\StockStatusInterface; -use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; -use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Select; use Magento\Store\Model\Store; -use Magento\Framework\App\ObjectManager; /** * Catalog search full test search data provider. @@ -129,16 +124,6 @@ class DataProvider */ private $antiGapMultiplier; - /** - * @var StockConfigurationInterface - */ - private $stockConfiguration; - - /** - * @var StockStatusRepositoryInterface - */ - private $stockStatusRepository; - /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -563,8 +548,6 @@ public function prepareProductIndex($indexData, $productData, $storeId) { $index = []; - $indexData = $this->filterOutOfStockProducts($indexData, $storeId); - foreach ($this->getSearchableAttributes('static') as $attribute) { $attributeCode = $attribute->getAttributeCode(); @@ -689,68 +672,4 @@ private function filterAttributeValue($value) { return preg_replace('/\s+/iu', ' ', trim(strip_tags($value))); } - - /** - * Filter out of stock products for products. - * - * @param array $indexData - * @param int $storeId - * @return array - */ - private function filterOutOfStockProducts($indexData, $storeId): array - { - if (!$this->getStockConfiguration()->isShowOutOfStock($storeId)) { - $productIds = array_keys($indexData); - $stockStatusCriteria = $this->createStockStatusCriteria(); - $stockStatusCriteria->setProductsFilter($productIds); - $stockStatusCollection = $this->getStockStatusRepository()->getList($stockStatusCriteria); - $stockStatuses = $stockStatusCollection->getItems(); - $stockStatuses = array_filter( - $stockStatuses, - function (StockStatusInterface $stockStatus) { - return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus(); - } - ); - $indexData = array_intersect_key($indexData, $stockStatuses); - } - return $indexData; - } - - /** - * Get stock configuration. - * - * @return StockConfigurationInterface - */ - private function getStockConfiguration() - { - if (null === $this->stockConfiguration) { - $this->stockConfiguration = ObjectManager::getInstance()->get(StockConfigurationInterface::class); - } - return $this->stockConfiguration; - } - - /** - * Create stock status criteria. - * - * Substitution of autogenerated factory in backward compatibility reasons. - * - * @return StockStatusCriteriaInterface - */ - private function createStockStatusCriteria() - { - return ObjectManager::getInstance()->create(StockStatusCriteriaInterface::class); - } - - /** - * Get stock status repository. - * - * @return StockStatusRepositoryInterface - */ - private function getStockStatusRepository() - { - if (null === $this->stockStatusRepository) { - $this->stockStatusRepository = ObjectManager::getInstance()->get(StockStatusRepositoryInterface::class); - } - return $this->stockStatusRepository; - } } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product/Category/Action/Rows.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product/Category/Action/Rows.php new file mode 100644 index 0000000000000..2b1844deb114c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product/Category/Action/Rows.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Product\Category\Action; + +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Catalog\Model\Indexer\Product\Category\Action\Rows as ActionRows; + +/** + * Catalog search indexer plugin for catalog category products assignment + */ +class Rows +{ + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @param IndexerRegistry $indexerRegistry + */ + public function __construct(IndexerRegistry $indexerRegistry) + { + $this->indexerRegistry = $indexerRegistry; + } + + /** + * Reindex after catalog category product reindex + * + * @param ActionRows $subject + * @param ActionRows $result + * @param array $entityIds + * @return ActionRows + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(ActionRows $subject, ActionRows $result, array $entityIds): ActionRows + { + if (!empty($entityIds)) { + $indexer = $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID); + if ($indexer->isScheduled()) { + $indexer->reindexList($entityIds); + } + } + return $result; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php b/app/code/Magento/CatalogSearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php new file mode 100644 index 0000000000000..02e48c5d8a1c0 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Model\Indexer\Plugin; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; + +/** + * Plugin for filtering child products that are out of stock for preventing their saving to catalog search index. + * + * This plugin reverts changes introduced in commit 9ab466d8569ea556cb01393989579c3aac53d9a3 which break extensions + * relying on stocks. Plugin location is changed for consistency purposes. + */ +class StockedProductsFilterPlugin +{ + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockStatusRepositoryInterface + */ + private $stockStatusRepository; + + /** + * @var StockStatusCriteriaInterfaceFactory + */ + private $stockStatusCriteriaFactory; + + /** + * @param StockConfigurationInterface $stockConfiguration + * @param StockStatusRepositoryInterface $stockStatusRepository + * @param StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory + */ + public function __construct( + StockConfigurationInterface $stockConfiguration, + StockStatusRepositoryInterface $stockStatusRepository, + StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory + ) { + $this->stockConfiguration = $stockConfiguration; + $this->stockStatusRepository = $stockStatusRepository; + $this->stockStatusCriteriaFactory = $stockStatusCriteriaFactory; + } + + /** + * Filter out of stock options for configurable product. + * + * @param DataProvider $dataProvider + * @param array $indexData + * @param array $productData + * @param int $storeId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforePrepareProductIndex( + DataProvider $dataProvider, + array $indexData, + array $productData, + int $storeId + ): array { + if (!$this->stockConfiguration->isShowOutOfStock($storeId)) { + $productIds = array_keys($indexData); + $stockStatusCriteria = $this->stockStatusCriteriaFactory->create(); + $stockStatusCriteria->setProductsFilter($productIds); + $stockStatusCollection = $this->stockStatusRepository->getList($stockStatusCriteria); + $stockStatuses = $stockStatusCollection->getItems(); + $stockStatuses = array_filter( + $stockStatuses, + function (StockStatusInterface $stockStatus) { + return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus(); + } + ); + $indexData = array_intersect_key($indexData, $stockStatuses); + } + + return [ + $indexData, + $productData, + $storeId, + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index 85a32dc60b119..d8947ac4224a8 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -58,15 +58,18 @@ public function apply(\Magento\Framework\App\RequestInterface $request) if (empty($attributeValue) && !is_numeric($attributeValue)) { return $this; } + $attribute = $this->getAttributeModel(); /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $productCollection */ $productCollection = $this->getLayer() ->getProductCollection(); $productCollection->addFieldToFilter($attribute->getAttributeCode(), $attributeValue); - $label = $this->getOptionText($attributeValue); - if (is_array($label)) { - $label = implode(',', $label); + + $labels = []; + foreach ((array) $attributeValue as $value) { + $labels[] = (array) $this->getOptionText($value); } + $label = implode(',', array_unique(array_merge(...$labels))); $this->getLayer() ->getState() ->addFilter($this->_createItem($label, $attributeValue)); diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php index e9fb1070fedd5..3b0c4dfb6df2f 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Layer\Filter; use Magento\Catalog\Model\Layer\Filter\AbstractFilter; @@ -12,6 +14,9 @@ */ class Decimal extends AbstractFilter { + /** Decimal delta for filter */ + private const DECIMAL_DELTA = 0.001; + /** * @var \Magento\Framework\Pricing\PriceCurrencyInterface */ @@ -70,11 +75,17 @@ public function apply(\Magento\Framework\App\RequestInterface $request) list($from, $to) = explode('-', $filter); + // When the range is 10-20 we only need to get products that are in the 10-19.99 range. + $toValue = $to; + if (!empty($toValue) && $from !== $toValue) { + $toValue -= self::DECIMAL_DELTA; + } + $this->getLayer() ->getProductCollection() ->addFieldToFilter( $this->getAttributeModel()->getAttributeCode(), - ['from' => $from, 'to' => $to] + ['from' => $from, 'to' => $toValue] ); $this->getLayer()->getState()->addFilter( @@ -111,7 +122,7 @@ protected function _getItemsData() $from = ''; } if ($to == '*') { - $to = null; + $to = ''; } $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); $value = $from . '-' . $to; @@ -138,7 +149,7 @@ protected function _getItemsData() protected function renderRangeLabel($fromPrice, $toPrice) { $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === null) { + if ($toPrice === '') { return __('%1 and above', $formattedFromPrice); } else { if ($fromPrice != $toPrice) { diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index a19f53469ae01..66d9281ed38e2 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Layer\Filter; use Magento\Catalog\Model\Layer\Filter\AbstractFilter; @@ -11,6 +13,7 @@ * Layer price filter based on Search API * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Price extends AbstractFilter { @@ -138,7 +141,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) list($from, $to) = $filter; $this->getLayer()->getProductCollection()->addFieldToFilter( - 'price', + $this->getAttributeModel()->getAttributeCode(), ['from' => $from, 'to' => empty($to) || $from == $to ? $to : $to - self::PRICE_DELTA] ); diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 1946dd35b8d37..bbebbc99103a2 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -27,6 +27,7 @@ use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; use Magento\Framework\App\ObjectManager; use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Search\Model\EngineResolver; /** * Advanced search collection @@ -40,6 +41,11 @@ */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { + /** + * Config search engine path. + */ + private const SEARCH_ENGINE_VALUE_PATH = 'catalog/search/engine'; + /** * List Of filters * @var array @@ -125,7 +131,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param Product\OptionFactory $productOptionFactory @@ -160,7 +166,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, @@ -344,6 +350,63 @@ protected function _renderFiltersBefore() parent::_renderFiltersBefore(); } + /** + * @inheritDoc + */ + public function clear() + { + $this->searchResult = null; + return parent::clear(); + } + + /** + * @inheritDoc + */ + protected function _reset() + { + $this->searchResult = null; + return parent::_reset(); + } + + /** + * @inheritdoc + */ + public function _loadEntities($printQuery = false, $logQuery = false) + { + $this->getEntity(); + + $currentSearchEngine = $this->_scopeConfig->getValue(self::SEARCH_ENGINE_VALUE_PATH); + if ($this->_pageSize && $currentSearchEngine === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) { + $this->getSelect()->limitPage($this->getCurPage(), $this->_pageSize); + } + + $this->printLogQuery($printQuery, $logQuery); + + try { + /** + * Prepare select query + * @var string $query + */ + $query = $this->getSelect(); + $rows = $this->_fetchAll($query); + } catch (\Exception $e) { + $this->printLogQuery(false, true, $query); + throw $e; + } + + foreach ($rows as $value) { + $object = $this->getNewEmptyItem()->setData($value); + $this->addItem($object); + if (isset($this->_itemsById[$object->getId()])) { + $this->_itemsById[$object->getId()][] = $object; + } else { + $this->_itemsById[$object->getId()] = [$object]; + } + } + + return $this; + } + /** * Get total records resolver. * @@ -391,7 +454,9 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se 'collection' => $this, 'searchResult' => $searchResult, /** This variable sets by serOrder method, but doesn't have a getter method. */ - 'orders' => $this->_orders + 'orders' => $this->_orders, + 'size' => $this->getPageSize(), + 'currentPage' => (int)$this->_curPage, ] ); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 4f84f3868c6a3..3506437ea038d 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -27,6 +27,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Search\Model\EngineResolver; /** * Fulltext Collection @@ -41,6 +42,11 @@ */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { + /** + * Config search engine path. + */ + private const SEARCH_ENGINE_VALUE_PATH = 'catalog/search/engine'; + /** * @var QueryResponse * @deprecated 100.1.0 @@ -146,7 +152,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -185,7 +191,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, @@ -212,10 +218,8 @@ public function __construct( DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null ) { $this->queryFactory = $catalogSearchData; - if ($searchResultFactory === null) { - $this->searchResultFactory = \Magento\Framework\App\ObjectManager::getInstance() + $this->searchResultFactory = $searchResultFactory ?? \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Api\Search\SearchResultFactory::class); - } parent::__construct( $entityFactory, $logger, @@ -375,6 +379,63 @@ public function addFieldToFilter($field, $condition = null) return $this; } + /** + * @inheritDoc + */ + public function clear() + { + $this->searchResult = null; + return parent::clear(); + } + + /** + * @inheritDoc + */ + protected function _reset() + { + $this->searchResult = null; + return parent::_reset(); + } + + /** + * @inheritdoc + */ + public function _loadEntities($printQuery = false, $logQuery = false) + { + $this->getEntity(); + + $currentSearchEngine = $this->_scopeConfig->getValue(self::SEARCH_ENGINE_VALUE_PATH); + if ($this->_pageSize && $currentSearchEngine === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) { + $this->getSelect()->limitPage($this->getCurPage(), $this->_pageSize); + } + + $this->printLogQuery($printQuery, $logQuery); + + try { + /** + * Prepare select query + * @var string $query + */ + $query = $this->getSelect(); + $rows = $this->_fetchAll($query); + } catch (\Exception $e) { + $this->printLogQuery(false, true, $query); + throw $e; + } + + foreach ($rows as $value) { + $object = $this->getNewEmptyItem()->setData($value); + $this->addItem($object); + if (isset($this->_itemsById[$object->getId()])) { + $this->_itemsById[$object->getId()][] = $object; + } else { + $this->_itemsById[$object->getId()] = [$object]; + } + } + + return $this; + } + /** * Add search query filter * @@ -427,25 +488,40 @@ protected function _renderFiltersBefore() return; } - $this->prepareSearchTermFilter(); - $this->preparePriceAggregation(); - - $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); - try { - $this->searchResult = $this->getSearch()->search($searchCriteria); - $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); - } catch (EmptyRequestDataException $e) { - /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ - $this->searchResult = $this->searchResultFactory->create()->setItems([]); - } catch (NonExistingRequestNameException $e) { - $this->_logger->error($e->getMessage()); - throw new LocalizedException(__('An error occurred. For details, see the error log.')); + if ($this->searchRequestName !== 'quick_search_container' + || strlen(trim($this->queryText)) + ) { + $this->prepareSearchTermFilter(); + $this->preparePriceAggregation(); + + $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); + try { + $this->searchResult = $this->getSearch()->search($searchCriteria); + $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); + } catch (EmptyRequestDataException $e) { + $this->searchResult = $this->createEmptyResult(); + } catch (NonExistingRequestNameException $e) { + $this->_logger->error($e->getMessage()); + throw new LocalizedException(__('An error occurred. For details, see the error log.')); + } + } else { + $this->searchResult = $this->createEmptyResult(); } $this->getSearchResultApplier($this->searchResult)->apply(); parent::_renderFiltersBefore(); } + /** + * Create empty search result + * + * @return SearchResultInterface + */ + private function createEmptyResult() + { + return $this->searchResultFactory->create()->setItems([]); + } + /** * Set sort order for search query. * @@ -485,12 +561,12 @@ private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface { return $this->searchCriteriaResolverFactory->create( [ - 'builder' => $this->getSearchCriteriaBuilder(), - 'collection' => $this, - 'searchRequestName' => $this->searchRequestName, - 'currentPage' => $this->_curPage, - 'size' => $this->getPageSize(), - 'orders' => $this->searchOrders, + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => (int)$this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, ] ); } @@ -505,10 +581,12 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se { return $this->searchResultApplierFactory->create( [ - 'collection' => $this, - 'searchResult' => $searchResult, - /** This variable sets by serOrder method, but doesn't have a getter method. */ - 'orders' => $this->_orders, + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders, + 'size' => $this->getPageSize(), + 'currentPage' => (int)$this->_curPage, ] ); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php index 6cdcc7c55a26f..e625ccbe51fe3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -50,7 +50,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -74,7 +74,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 8f8ba39ebd329..5ac252677ff79 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Search; use Magento\Catalog\Api\Data\EavAttributeInterface; @@ -78,6 +80,7 @@ private function generateRequest($attributeType, $container, $useFulltext) { $request = []; foreach ($this->getSearchableAttributes() as $attribute) { + /** @var $attribute Attribute */ if ($attribute->getData($attributeType)) { if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'], true)) { $queryName = $attribute->getAttributeCode() . '_query'; @@ -97,12 +100,14 @@ private function generateRequest($attributeType, $container, $useFulltext) ], ]; $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; - $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + $generatorType = $attribute->getFrontendInput() === 'price' + ? $attribute->getFrontendInput() + : $attribute->getBackendType(); + $generator = $this->generatorResolver->getGeneratorForType($generatorType); $request['filters'][$filterName] = $generator->getFilterData($attribute, $filterName); $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); } } - /** @var $attribute Attribute */ if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price'], true)) { // Some fields have their own specific handlers continue; diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php index b3d39a48fe9fc..73d011cc532db 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogSearch\Model\Search\RequestGenerator; diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php new file mode 100644 index 0000000000000..949806d14f45a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Model\Search\RequestGenerator; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Search\Request\BucketInterface; +use Magento\Framework\Search\Request\FilterInterface; + +/** + * Catalog search range request generator. + */ +class Price implements GeneratorInterface +{ + /** + * @inheritdoc + */ + public function getFilterData(Attribute $attribute, $filterName): array + { + return [ + 'type' => FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * @inheritdoc + */ + public function getAggregationData(Attribute $attribute, $bucketName): array + { + return [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attribute->getAttributeCode(), + 'method' => '$price_dynamic_algorithm$', + 'metric' => [['type' => 'count']], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php index 7f6dbe033e3a5..21d5e82d494b5 100644 --- a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php +++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php @@ -6,18 +6,21 @@ namespace Magento\CatalogSearch\Setup\Patch\Data; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Framework\App\State; +use Magento\Framework\Indexer\IndexerInterfaceFactory; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\Framework\Indexer\IndexerInterfaceFactory; -use Magento\Catalog\Api\ProductAttributeRepositoryInterface; /** + * This patch sets up search weight for the product's system attributes, reindex required after patch applying. + * * @deprecated * @see \Magento\ElasticSearch */ class SetInitialSearchWeightForAttributes implements DataPatchInterface, PatchVersionInterface { + /** * @var IndexerInterfaceFactory */ @@ -50,7 +53,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -60,13 +63,15 @@ public function apply() $this->state->emulateAreaCode( \Magento\Framework\App\Area::AREA_CRONTAB, function () use ($indexer) { - $indexer->reindexAll(); + $indexer->getState() + ->setStatus(\Magento\Framework\Indexer\StateInterface::STATUS_INVALID) + ->save(); } ); } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -74,7 +79,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -82,7 +87,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml index a72762ff796e0..2022f809139ec 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml @@ -41,6 +41,27 @@ <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> </actionGroup> + <actionGroup name="StorefrontQuickSearchTooShortStringActionGroup" extends="StorefrontCheckQuickSearchStringActionGroup"> + <annotations> + <description>Fill the Storefront Search field. Submits the Form. Validates that 'Minimum Search query length' warning appears.</description> + </annotations> + <arguments> + <argument name="minQueryLength" type="string"/> + </arguments> + + <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="Minimum Search query length is {{minQueryLength}}" stepKey="assertQuickSearchNeedThreeOrMoreChars"/> + </actionGroup> + + <actionGroup name="StorefrontQuickSearchRelatedSearchTermsAppearsActionGroup"> + <arguments> + <argument name="term" type="string"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontCatalogSearchMainSection.relatedSearchTermsTitle}}" stepKey="waitMessageAppears"/> + <see selector="{{StorefrontCatalogSearchMainSection.relatedSearchTermsTitle}}" userInput="Related search terms" stepKey="checkRelatedTermsTitle"/> + <see selector="{{StorefrontCatalogSearchMainSection.relatedSearchTerm}}" userInput="{{term}}" stepKey="checkRelatedTermExists"/> + </actionGroup> + <!-- Opens product from QuickSearch and performs assertions--> <actionGroup name="StorefrontOpenProductFromQuickSearch"> <annotations> @@ -51,6 +72,7 @@ <argument name="productUrlKey" type="string"/> </arguments> + <scrollTo selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}" stepKey="scrollToProduct"/> <click stepKey="openProduct" selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}"/> <waitForPageLoad stepKey="waitForProductLoad"/> <seeInCurrentUrl url="{{productUrlKey}}" stepKey="checkUrl"/> @@ -66,7 +88,8 @@ <argument name="productName" type="string"/> </arguments> - <moveMouseOver stepKey="hoverOverProduct" selector="{{StorefrontQuickSearchResultsSection.productByIndex('1')}}"/> + <scrollTo selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}" stepKey="scrollToProduct"/> + <moveMouseOver stepKey="hoverOverProduct" selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}"/> <click selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}} {{StorefrontQuickSearchResultsSection.addToCartBtn}}" stepKey="addToCart"/> <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> @@ -101,7 +124,7 @@ <dontSee selector="{{StorefrontQuickSearchResultsSection.allResults}}" userInput="{{productName}}" stepKey="dontSeeProductName"/> </actionGroup> - + <!-- Open advanced search page --> <actionGroup name="StorefrontOpenAdvancedSearchActionGroup"> <annotations> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml new file mode 100644 index 0000000000000..1afdb6e5e46fa --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml @@ -0,0 +1,29 @@ +<?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="StorefrontFillFormAdvancedSearchActionGroup"> + <arguments> + <argument name="productName" type="string" defaultValue=""/> + <argument name="sku" type="string" defaultValue=""/> + <argument name="description" type="string" defaultValue=""/> + <argument name="short_description" type="string" defaultValue=""/> + <argument name="price_from" type="string" defaultValue=""/> + <argument name="price_to" type="string" defaultValue=""/> + </arguments> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.ProductName}}" userInput="{{productName}}" stepKey="fillName"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.SKU}}" userInput="{{sku}}" stepKey="fillSku"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.Description}}" userInput="{{description}}" stepKey="fillDescription"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.ShortDescription}}" userInput="{{short_description}}" stepKey="fillShortDescription"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceFrom}}" userInput="{{price_from}}" stepKey="fillPriceFrom"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceTo}}" userInput="{{price_to}}" stepKey="fillPriceTo"/> + <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="clickSubmit"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml index 6868456079110..9dab06ffb14f0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml @@ -23,5 +23,10 @@ <entity name="SetMinQueryLengthToOne" type="number"> <data key="value">1</data> </entity> - + <entity name="SetCatalogSearchEngineToDefault" type="catalog_search_engine_default"> + <requiredEntity type="enable">DefaultCatalogSearchEngine</requiredEntity> + </entity> + <entity name="DefaultCatalogSearchEngine" type="enable"> + <data key="inherit">true</data> + </entity> </entities> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..dd8c426592619 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,19 @@ +<?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="MinimalQueryLengthDefaultConfigData"> + <data key="path">catalog/search/min_query_length</data> + <data key="value">3</data> + </entity> + <entity name="MinimalQueryLengthFourConfigData"> + <data key="path">catalog/search/min_query_length</data> + <data key="value">4</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml index 7405377249aa4..ce869f81a23df 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml @@ -29,4 +29,15 @@ </object> </object> </operation> + <operation name="CatalogSearchEngineDefault" dataType="catalog_search_engine_default" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_search_engine_default"> + <object key="search" dataType="catalog_search_engine_default"> + <object key="fields" dataType="catalog_search_engine_default"> + <object key="engine" dataType="enable"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> </operations> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml index 6b28b4f36c6a7..eb3bc8e79d7b5 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml @@ -17,5 +17,6 @@ <element name="message" type="text" selector="div.message div"/> <element name="itemFound" type="text" selector=".search.found>strong"/> <element name="productName" type="text" selector=".product.name.product-item-name>a"/> + <element name="nthProductName" type="text" selector="li.product-item:nth-of-type({{var1}}) .product-item-name>a" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml index 667f08fea6579..b005e100b30bb 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml @@ -16,5 +16,7 @@ <element name="productCount" type="text" selector="#toolbar-amount"/> <element name="message" type="text" selector="div.message div"/> <element name="searchResults" type="block" selector="#maincontent .column.main"/> + <element name="relatedSearchTermsTitle" type="text" selector="div.message dl.block dt.title"/> + <element name="relatedSearchTerm" type="text" selector="div.message dl.block dd.item a"/> </section> </sections> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml index 13665100f79af..0e92d9fb0c7ad 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml @@ -13,6 +13,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> <argument name="name" value="$$product.name$$"/> @@ -26,6 +31,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> <argument name="sku" value="$$product.sku$$"/> @@ -39,6 +49,10 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> <argument name="description" value="$$product.custom_attributes[description]$$"/> @@ -52,6 +66,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> <argument name="shortDescription" value="$$product.custom_attributes[short_description]$$"/> @@ -65,6 +84,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> <argument name="name" value="$$arg1.name$$"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 99f3fc00a7401..aa7cf933f6328 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -9,6 +9,72 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CGuestUserTest"> + <!-- Step 2: User searches for product --> + <comment userInput="Start of searching products" stepKey="startOfSearchingProducts" after="endOfBrowsingCatalog"/> + <!-- Advanced Search with Product Data --> + <comment userInput="Advanced search" stepKey="commentAdvancedSearch" after="startOfSearchingProducts"/> + <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="searchOpenAdvancedSearchForm" after="commentAdvancedSearch"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <fillField userInput="$$createSimpleProduct1.name$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.ProductName}}" stepKey="searchAdvancedFillProductName" after="searchOpenAdvancedSearchForm"/> + <fillField userInput="$$createSimpleProduct1.sku$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.SKU}}" stepKey="searchAdvancedFillSKU" after="searchAdvancedFillProductName"/> + <fillField userInput="$$createSimpleProduct1.price$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceFrom}}" stepKey="searchAdvancedFillPriceFrom" after="searchAdvancedFillSKU"/> + <fillField userInput="$$createSimpleProduct1.price$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceTo}}" stepKey="searchAdvancedFillPriceTo" after="searchAdvancedFillPriceFrom"/> + <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="searchClickAdvancedSearchSubmitButton" after="searchAdvancedFillPriceTo"/> + <waitForLoadingMaskToDisappear stepKey="waitForSearchProductsloaded" after="searchClickAdvancedSearchSubmitButton"/> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="searchCheckAdvancedSearchResult" after="waitForSearchProductsloaded"/> + <see userInput="4" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productCount}} span" stepKey="searchAdvancedAssertProductCount" after="searchCheckAdvancedSearchResult"/> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="searchAssertSimpleProduct1" after="searchAdvancedAssertProductCount"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="searchAdvancedGrabSimpleProduct1ImageSrc" after="searchAssertSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchAdvancedGrabSimpleProduct1ImageSrc" stepKey="searchAdvancedAssertSimpleProduct1ImageNotDefault" after="searchAdvancedGrabSimpleProduct1ImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="searchClickSimpleProduct1View" after="searchAdvancedAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForSearchSimpleProduct1Viewloaded" after="searchClickSimpleProduct1View"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="searchAssertSimpleProduct1Page" after="waitForSearchSimpleProduct1Viewloaded"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchAdvancedGrabSimpleProduct1PageImageSrc" after="searchAssertSimpleProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchAdvancedGrabSimpleProduct1PageImageSrc" stepKey="searchAdvancedAssertSimpleProduct1PageImageNotDefault" after="searchAdvancedGrabSimpleProduct1PageImageSrc"/> + + <!-- Quick Search with common part of product names --> + <comment userInput="Quick search" stepKey="commentQuickSearch" after="searchAdvancedAssertSimpleProduct1PageImageNotDefault"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="searchQuickSearchCommonPart" after="commentQuickSearch"> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="phrase" value="CONST.apiSimpleProduct"/> + </actionGroup> + <actionGroup ref="StorefrontSelectSearchFilterCategoryActionGroup" stepKey="searchSelectFilterCategoryCommonPart" after="searchQuickSearchCommonPart"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <see userInput="3" selector="{{StorefrontCategoryMainSection.productCount}} span" stepKey="searchAssertFilterCategoryProductCountCommonPart" after="searchSelectFilterCategoryCommonPart"/> + + <!-- Search simple product 1 --> + <comment userInput="Search simple product 1" stepKey="commentSearchSimpleProduct1" after="searchAssertFilterCategoryProductCountCommonPart"/> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="searchAssertFilterCategorySimpleProduct1" after="commentSearchSimpleProduct1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="searchGrabSimpleProduct1ImageSrc" after="searchAssertFilterCategorySimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchGrabSimpleProduct1ImageSrc" stepKey="searchAssertSimpleProduct1ImageNotDefault" after="searchGrabSimpleProduct1ImageSrc"/> + <!-- Search simple product2 --> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="searchAssertFilterCategorySimpleProduct2" after="searchAssertSimpleProduct1ImageNotDefault"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="searchGrabSimpleProduct2ImageSrc" after="searchAssertFilterCategorySimpleProduct2"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchGrabSimpleProduct2ImageSrc" stepKey="searchAssertSimpleProduct2ImageNotDefault" after="searchGrabSimpleProduct2ImageSrc"/> + + <!-- Quick Search with non-existent product name --> + <comment userInput="Quick Search with non-existent product name" stepKey="commentQuickSearchWithNonExistentProductName" after="searchAssertSimpleProduct2ImageNotDefault" /> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="searchFillQuickSearchNonExistent" after="commentQuickSearchWithNonExistentProductName"> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="phrase" value="CONST.nonexistentProductName"/> + </actionGroup> + <see userInput="Your search returned no results." selector="{{StorefrontCatalogSearchMainSection.message}}" stepKey="searchAssertQuickSearchMessageNonExistent" after="searchFillQuickSearchNonExistent"/> + <comment userInput="End of searching products" stepKey="endOfSearchingProducts" after="searchAssertQuickSearchMessageNonExistent" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> <!-- Step 2: User searches for product --> <comment userInput="Start of searching products" stepKey="startOfSearchingProducts" after="endOfBrowsingCatalog"/> <!-- Advanced Search with Product 1 Data --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml new file mode 100644 index 0000000000000..c8f84c732d6ba --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml @@ -0,0 +1,92 @@ +<?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="LayerNavigationOfCatalogSearchTest"> + <annotations> + <stories value="Search terms"/> + <title value="Layer Navigation of Catalog Search Should Equalize Price Range As Default Configuration"/> + <description value="Make sure filter of custom attribute with type of price displays on storefront Catalog page and price range should respect the configuration in Admin site"/> + <testCaseId value="MC-16979"/> + <useCaseId value="MC-16650"/> + <severity value="MAJOR"/> + <group value="CatalogSearch"/> + </annotations> + <before> + <magentoCLI command="config:set catalog/layered_navigation/price_range_calculation auto" stepKey="setAutoPriceRange"/> + <createData stepKey="createPriceAttribute" entity="productAttributeTypeOfPrice"/> + <createData stepKey="assignPriceAttributeGroup" entity="AddToDefaultSet"> + <requiredEntity createDataKey="createPriceAttribute"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="subCategory"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="subCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="subCategory"/> + </createData> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="subCategory"/> + <deleteData stepKey="deleteSimpleProduct1" createDataKey="simpleProduct1"/> + <deleteData stepKey="deleteSimpleProduct2" createDataKey="simpleProduct2"/> + <deleteData createDataKey="createPriceAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Update value for price attribute of Product 1--> + <comment userInput="Update value for price attribute of Product 1" stepKey="comment1"/> + <actionGroup ref="navigateToCreatedProductEditPage" stepKey="navigateToCreatedProductEditPage1"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminProductFormSection.attributeLabelByText($$createPriceAttribute.attribute[frontend_labels][0][label]$$)}}" stepKey="grabAttributeLabel"/> + <fillField selector="{{AdminProductAttributeSection.customAttribute($$createPriceAttribute.attribute_code$$)}}" userInput="30" stepKey="fillCustomPrice1"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton1"/> + <waitForPageLoad stepKey="waitForSimpleProductSaved1"/> + <!--Update value for price attribute of Product 2--> + <comment userInput="Update value for price attribute of Product 1" stepKey="comment2"/> + <actionGroup ref="navigateToCreatedProductEditPage" stepKey="navigateToCreatedProductEditPage2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <fillField selector="{{AdminProductAttributeSection.customAttribute($$createPriceAttribute.attribute_code$$)}}" userInput="70" stepKey="fillCustomPrice2"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton2"/> + <waitForPageLoad stepKey="waitForSimpleProductSaved2"/> + + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!--Navigate to category on Storefront--> + <comment userInput="Navigate to category on Storefront" stepKey="comment3"/> + <amOnPage url="{{StorefrontCategoryPage.url($$subCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="{$grabAttributeLabel}" selector="{{StorefrontCategoryFilterSection.CustomPriceAttribute}}" stepKey="seePriceLayerNavigationOnStorefront"/> + </test> +</tests> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml index b6417e12a6db7..89269a1ad0d9e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6325"/> <useCaseId value="MAGETWO-58764"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml index 19db201e91f40..3b60e4b09de28 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml @@ -23,6 +23,10 @@ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> @@ -74,6 +78,7 @@ </test> <test name="QuickSearchEmptyResults"> <annotations> + <features value="CatalogSearch"/> <stories value="Search Product on Storefront"/> <title value="User should not get search results on query that doesn't return anything"/> <description value="Use invalid query to return no products"/> @@ -87,19 +92,26 @@ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> - <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> - <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="ThisShouldn'tReturnAnything"/> </actionGroup> <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmpty"/> </test> + <test name="QuickSearchWithTwoCharsEmptyResults" extends="QuickSearchEmptyResults"> <annotations> + <features value="CatalogSearch"/> <stories value="Search Product on Storefront"/> <title value="User should not get search results on query that only contains two characters"/> <description value="Use of 2 character query to return no products"/> @@ -107,15 +119,30 @@ <testCaseId value="MC-14794"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-15827"/> - </skip> </annotations> - <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,2); return ret;" stepKey="getFirstTwoLetters" before="searchStorefront"/> - <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> - <argument name="phrase" value="{$getFirstTwoLetters}"/> + + <before> + <magentoCLI command="config:set {{MinimalQueryLengthFourConfigData.path}} {{MinimalQueryLengthFourConfigData.value}}" after="createSimpleProduct" stepKey="setMinimalQueryLengthToFour"/> + </before> + + <after> + <magentoCLI command="config:set {{MinimalQueryLengthDefaultConfigData.path}} {{MinimalQueryLengthDefaultConfigData.value}}" after="deleteCategory" stepKey="setMinimalQueryLengthToFour"/> + </after> + + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,{{MinimalQueryLengthFourConfigData.value}} - 1); return ret;" before="searchStorefront" stepKey="getFirstLessThenConfigLetters"/> + + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" after="checkEmpty" stepKey="searchStorefrontConfigLetters"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchTooShortStringActionGroup" after="searchStorefrontConfigLetters" stepKey="checkCannotSearchWithTooShortString"> + <argument name="phrase" value="$getFirstLessThenConfigLetters"/> + <argument name="minQueryLength" value="{{MinimalQueryLengthFourConfigData.value}}"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchRelatedSearchTermsAppearsActionGroup" after="checkCannotSearchWithTooShortString" stepKey="checkRelatedSearchTerm"> + <argument name="term" value="$createSimpleProduct.name$"/> </actionGroup> </test> + <test name="QuickSearchProductByNameWithThreeLetters" extends="QuickSearchProductBySku"> <annotations> <stories value="Search Product on Storefront"/> @@ -124,6 +151,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-15034"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> <group value="mtf_migrated"/> </annotations> <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,3); return ret;" stepKey="getFirstThreeLetters" before="searchStorefront"/> @@ -160,6 +188,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-14796"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> <group value="mtf_migrated"/> </annotations> <before> @@ -242,6 +271,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-14797"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> <group value="mtf_migrated"/> </annotations> <before> @@ -306,6 +336,10 @@ <createData entity="VirtualProduct" stepKey="createVirtualProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> @@ -336,12 +370,18 @@ <argument name="product" value="_defaultProduct"/> <argument name="category" value="$$createCategory$$"/> </actionGroup> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> <argument name="sku" value="{{_defaultProduct.sku}}"/> </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> </after> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> @@ -368,6 +408,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> @@ -375,8 +416,13 @@ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="createProduct"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> @@ -399,19 +445,27 @@ <group value="mtf_migrated"/> </annotations> <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> <createData entity="ApiProductWithDescription" stepKey="simple1"/> <createData entity="ApiGroupedProduct" stepKey="createProduct"/> <createData entity="OneSimpleProductLink" stepKey="addProductOne"> <requiredEntity createDataKey="createProduct"/> <requiredEntity createDataKey="simple1"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> - <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteGroupedProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simple1"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> </after> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> - <argument name="phrase" value="$createProduct.name$"/> + <argument name="phrase" value=""$createProduct.name$""/> </actionGroup> <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> <argument name="productName" value="$createProduct.name$"/> @@ -450,11 +504,16 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> </after> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> @@ -512,11 +571,18 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> </after> <comment userInput="$simpleProduct1.name$" stepKey="asdf"/> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> @@ -601,6 +667,10 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml new file mode 100644 index 0000000000000..9ad868ff6db7e --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.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="StorefrontAdvancedSearchByAllParametersTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by name, sku, description, short description, price from and price to"/> + <description value="Search product in advanced search by name, sku, description, short description, price from and price to"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="sku" value="abc_dfj"/> + <argument name="description" value="adc_Full"/> + <argument name="short_description" value="abc_short"/> + <argument name="price_to" value="500"/> + <argument name="price_from" value="49"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml new file mode 100644 index 0000000000000..5693721e6ed65 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml @@ -0,0 +1,28 @@ +<?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="StorefrontAdvancedSearchByDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by description"/> + <description value="Search product in advanced search by description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ABC_123_SimpleProduct" stepKey="createProduct"/> + </before> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="description" value="dfj_full"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml new file mode 100644 index 0000000000000..4d3ba22f79356 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.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="StorefrontAdvancedSearchByNameSkuDescriptionPriceTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by name, sku, description, short description, price from 49 and price to 50"/> + <description value="Search product in advanced search by name, sku, description, short description, price from 49 and price to 50"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="sku" value="abc_dfj"/> + <argument name="description" value="adc_Full"/> + <argument name="short_description" value="abc_short"/> + <argument name="price_to" value="50"/> + <argument name="price_from" value="49"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml new file mode 100644 index 0000000000000..f0b81e08252fc --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml @@ -0,0 +1,28 @@ +<?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="StorefrontAdvancedSearchByNameTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by name"/> + <description value="Search product in advanced search by name"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ABC_123_SimpleProduct" stepKey="createProduct"/> + </before> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml new file mode 100644 index 0000000000000..f875021bd9669 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml @@ -0,0 +1,26 @@ +<?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="StorefrontAdvancedSearchByPartialNameTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial name"/> + <description value="Search product in advanced search by partial name"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + <group value="SearchEngineMysql"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="abc"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml new file mode 100644 index 0000000000000..0edc3f31216bb --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml @@ -0,0 +1,25 @@ +<?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="StorefrontAdvancedSearchByPartialShortDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial short description"/> + <description value="Search product in advanced search by partial short description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="short_description" value="abc_short"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml new file mode 100644 index 0000000000000..b2b4ef9cc4782 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml @@ -0,0 +1,26 @@ +<?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="StorefrontAdvancedSearchByPartialSkuAndDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial sku and description"/> + <description value="Search product in advanced search by partial sku and description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="sku" value="abc"/> + <argument name="description" value="adc_full"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml new file mode 100644 index 0000000000000..45cec0a899361 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml @@ -0,0 +1,25 @@ +<?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="StorefrontAdvancedSearchByPartialSkuTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial sku"/> + <description value="Search product in advanced search by partial sku"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="sku" value="abc"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml new file mode 100644 index 0000000000000..6b85cdf61c84c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml @@ -0,0 +1,26 @@ +<?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="StorefrontAdvancedSearchByPriceFromAndPriceToTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by price from and price to"/> + <description value="Search product in advanced search by price from and price to"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="price_to" value="50"/> + <argument name="price_from" value="50"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml new file mode 100644 index 0000000000000..33dff8aefa334 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.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="StorefrontAdvancedSearchByPriceToTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by price to"/> + <description value="Search product in advanced search by price to"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ABC_123_SimpleProduct" stepKey="createProduct2" after="createProduct"/> + </before> + <after> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2" after="deleteProduct"/> + </after> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="price_to" value="100"/> + </actionGroup> + <!-- See that some items were found, other products may exist besides our test products --> + <see userInput="items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$createProduct2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProduct2Name" after="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml new file mode 100644 index 0000000000000..c4622d02a5152 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml @@ -0,0 +1,29 @@ +<?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="StorefrontAdvancedSearchByShortDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by short description"/> + <description value="Search product in advanced search by short description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <remove keyForRemoval="createProduct"/> + <remove keyForRemoval="deleteProduct"/> + <remove keyForRemoval="seeProductName"/> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="short_description" value="dfj_short"/> + </actionGroup> + <see userInput="We can't find any items matching these search criteria. Modify your search." selector="{{StorefrontQuickSearchResultsSection.messageSection}}" stepKey="see"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml new file mode 100644 index 0000000000000..ca5e237099681 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml @@ -0,0 +1,25 @@ +<?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="StorefrontAdvancedSearchBySkuTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by sku"/> + <description value="Search product in advanced search by sku"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="sku" value="abc_dfj"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml new file mode 100644 index 0000000000000..14df2133017d9 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml @@ -0,0 +1,55 @@ +<?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="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Use Advanced Search to Find the Product"/> + <description value="Use Advanced Search to Find the Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-12421"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Delete all products left by prev tests because it sensitive for search--> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> + <!-- Create Data --> + <createData entity="ABC_dfj_SimpleProduct" stepKey="createProduct"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logoutAdmin"/> + </after> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- 1. Navigate to Frontend --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + + <!-- 2. Click "Advanced Search" --> + <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> + + <!-- 3. Fill test data in to field(s) 4. Click "Search" button--> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="sku" value="abc_dfj"/> + </actionGroup> + + <!-- 5. Perform all asserts --> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$createProduct.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml new file mode 100644 index 0000000000000..b4f2314295a00 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml @@ -0,0 +1,29 @@ +<?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="StorefrontAdvancedSearchNegativeProductSearchTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Negative product search"/> + <description value="Negative product search"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <remove keyForRemoval="createProduct"/> + <remove keyForRemoval="deleteProduct"/> + <remove keyForRemoval="seeProductName"/> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="Negative_product_search"/> + </actionGroup> + <see userInput="We can't find any items matching these search criteria. Modify your search." selector="{{StorefrontQuickSearchResultsSection.messageSection}}" stepKey="see"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml new file mode 100644 index 0000000000000..8a29ab718bd25 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml @@ -0,0 +1,33 @@ +<?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="StorefrontAdvancedSearchWithoutEnteringDataTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Do Advanced Search without entering data"/> + <description value="'Enter a search term and try again.' error message is missed in Advanced Search"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-14859"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <!-- 1. Navigate to Frontend --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + + <!-- 2. Click "Advanced Search" --> + <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> + + <!-- 3. Fill test data in to field(s) 4. Click "Search" button--> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"/> + + <!-- 5. Perform all asserts --> + <see userInput="Enter a search term and try again." selector="{{StorefrontQuickSearchResultsSection.messageSection}}" stepKey="see"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index 7e3de7534e8c4..a79ffcc33cabe 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -129,7 +129,7 @@ protected function setUp() ->getMock(); $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->disableOriginalConstructor() - ->setMethods(['select', 'getIfNullSql', 'quote']) + ->setMethods(['select', 'getIfNullSql', 'quote', 'quoteInto']) ->getMockForAbstractClass(); $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() @@ -222,9 +222,10 @@ public function testProcessPrice() public function processCategoryIdsDataProvider() { return [ - ['5', 'category_ids_index.category_id = 5'], - [3, 'category_ids_index.category_id = 3'], - ["' and 1 = 0", 'category_ids_index.category_id = 0'], + ['5', "category_ids_index.category_id in ('5')"], + [3, "category_ids_index.category_id in (3)"], + ["' and 1 = 0", "category_ids_index.category_id in ('\' and 1 = 0')"], + [['5', '10'], "category_ids_index.category_id in ('5', '10')"] ]; } @@ -251,6 +252,12 @@ public function testProcessCategoryIds($categoryId, $expectedResult) ->with(\Magento\Catalog\Model\Product::ENTITY, 'category_ids') ->will($this->returnValue($this->attribute)); + $this->connection + ->expects($this->once()) + ->method('quoteInto') + ->with('category_ids_index.category_id in (?)', $categoryId) + ->willReturn($expectedResult); + $actualResult = $this->target->process($this->filter, $isNegation, $query); $this->assertSame($expectedResult, $this->removeWhitespaces($actualResult)); } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php new file mode 100644 index 0000000000000..b9909ec2c74b2 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Test\Unit\Model\Indexer\Plugin; + +use Magento\CatalogSearch\Model\Indexer\Plugin\StockedProductsFilterPlugin; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; +use Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; + +/** + * Test for Magento\CatalogSearch\Model\Indexer\Plugin\StockedProductsFilterPlugin class. + * + * This plugin reverts changes introduced in commit 9ab466d8569ea556cb01393989579c3aac53d9a3 which break extensions + * relying on stocks. Plugin location is changed for consistency purposes. + */ +class StockedProductsFilterPluginTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockConfigurationMock; + + /** + * @var StockStatusRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockStatusRepositoryMock; + + /** + * @var StockStatusCriteriaInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockStatusCriteriaFactoryMock; + + /** + * @var StockedProductsFilterPlugin + */ + private $plugin; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockStatusRepositoryMock = $this->getMockBuilder(StockStatusRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockStatusCriteriaFactoryMock = $this->getMockBuilder(StockStatusCriteriaInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->plugin = new StockedProductsFilterPlugin( + $this->stockConfigurationMock, + $this->stockStatusRepositoryMock, + $this->stockStatusCriteriaFactoryMock + ); + } + + /** + * @return void + */ + public function testBeforePrepareProductIndex(): void + { + /** @var DataProvider|\PHPUnit_Framework_MockObject_MockObject $dataProviderMock */ + $dataProviderMock = $this->getMockBuilder(DataProvider::class)->disableOriginalConstructor()->getMock(); + $indexData = [ + 1 => [], + 2 => [], + ]; + $productData = []; + $storeId = 1; + + $this->stockConfigurationMock + ->expects($this->once()) + ->method('isShowOutOfStock') + ->willReturn(false); + + $stockStatusCriteriaMock = $this->getMockBuilder(StockStatusCriteriaInterface::class)->getMock(); + $stockStatusCriteriaMock + ->expects($this->once()) + ->method('setProductsFilter') + ->willReturn(true); + $this->stockStatusCriteriaFactoryMock + ->expects($this->once()) + ->method('create') + ->willReturn($stockStatusCriteriaMock); + + $stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)->getMock(); + $stockStatusMock->expects($this->atLeastOnce()) + ->method('getStockStatus') + ->willReturnOnConsecutiveCalls(Stock::STOCK_IN_STOCK, Stock::STOCK_OUT_OF_STOCK); + $stockStatusCollectionMock = $this->getMockBuilder(StockStatusCollectionInterface::class)->getMock(); + $stockStatusCollectionMock + ->expects($this->once()) + ->method('getItems') + ->willReturn([1 => $stockStatusMock, 2 => $stockStatusMock]); + $this->stockStatusRepositoryMock + ->expects($this->once()) + ->method('getList') + ->willReturn($stockStatusCollectionMock); + + list ($indexData, $productData, $storeId) = $this->plugin->beforePrepareProductIndex( + $dataProviderMock, + $indexData, + $productData, + $storeId + ); + + $this->assertEquals([1], array_keys($indexData)); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php index abad58a6876d3..f783f75a170e3 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogSearch\Test\Unit\Model\Layer\Filter; @@ -208,6 +209,12 @@ public function testApply() $priceId = '15-50'; $requestVar = 'test_request_var'; + $this->target->setAttributeModel($this->attribute); + $attributeCode = 'price'; + $this->attribute->expects($this->any()) + ->method('getAttributeCode') + ->will($this->returnValue($attributeCode)); + $this->target->setRequestVar($requestVar); $this->request->expects($this->exactly(1)) ->method('getParam') diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php index 683070c286239..10010188c26c9 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php @@ -14,6 +14,7 @@ use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use PHPUnit\Framework\MockObject\MockObject; /** * Tests Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection @@ -35,32 +36,37 @@ class CollectionTest extends BaseCollection private $advancedCollection; /** - * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\FilterBuilder|MockObject */ private $filterBuilder; /** - * @var \Magento\Framework\Api\Search\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\Search\SearchCriteriaBuilder|MockObject */ private $criteriaBuilder; /** - * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory|MockObject */ private $temporaryStorageFactory; /** - * @var \Magento\Search\Api\SearchInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Search\Api\SearchInterface|MockObject */ private $search; /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Eav\Model\Config|MockObject */ private $eavConfig; /** - * setUp method for CollectionTest + * @var SearchResultApplierFactory|MockObject + */ + private $searchResultApplierFactory; + + /** + * @inheritdoc */ protected function setUp() { @@ -97,17 +103,10 @@ protected function setUp() ->method('create') ->willReturn($searchCriteriaResolver); - $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) - ->disableOriginalConstructor() - ->setMethods(['apply']) - ->getMockForAbstractClass(); - $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + $this->searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $searchResultApplierFactory->expects($this->any()) - ->method('create') - ->willReturn($searchResultApplier); $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) ->disableOriginalConstructor() @@ -134,12 +133,15 @@ protected function setUp() 'productLimitationFactory' => $productLimitationFactoryMock, 'collectionProvider' => null, 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, - 'searchResultApplierFactory' => $searchResultApplierFactory, + 'searchResultApplierFactory' => $this->searchResultApplierFactory, 'totalRecordsResolverFactory' => $totalRecordsResolverFactory ] ); } + /** + * Test to Load data with filter in place + */ public function testLoadWithFilterNoFilters() { $this->advancedCollection->loadWithFilter(); @@ -150,6 +152,7 @@ public function testLoadWithFilterNoFilters() */ public function testLike() { + $pageSize = 10; $attributeCode = 'description'; $attributeCodeId = 42; $attribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); @@ -168,6 +171,23 @@ public function testLike() $searchResult = $this->createMock(\Magento\Framework\Api\Search\SearchResultInterface::class); $this->search->expects($this->once())->method('search')->willReturn($searchResult); + $this->advancedCollection->setPageSize($pageSize); + $this->advancedCollection->setCurPage(0); + + $searchResultApplier = $this->createMock(SearchResultApplierInterface::class); + $this->searchResultApplierFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'collection' => $this->advancedCollection, + 'searchResult' => $searchResult, + 'orders' => [], + 'size' => $pageSize, + 'currentPage' => 0, + ] + ) + ->willReturn($searchResultApplier); + // addFieldsToFilter will load filters, // then loadWithFilter will trigger _renderFiltersBefore code in Advanced/Collection $this->assertSame( @@ -177,7 +197,7 @@ public function testLike() } /** - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function getCriteriaBuilder() { @@ -185,6 +205,7 @@ protected function getCriteriaBuilder() ->setMethods(['addFilter', 'create', 'setRequestName']) ->disableOriginalConstructor() ->getMock(); + return $criteriaBuilder; } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php index 9170b81dc3182..9b4010cfae453 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; @@ -12,11 +13,12 @@ use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; +use PHPUnit\Framework\MockObject\MockObject; use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; -use PHPUnit_Framework_MockObject_MockObject as MockObject; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; /** + * Test class for Fulltext Collection + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CollectionTest extends BaseCollection @@ -27,12 +29,12 @@ class CollectionTest extends BaseCollection private $objectManager; /** - * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorage|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorage|MockObject */ private $temporaryStorage; /** - * @var \Magento\Search\Api\SearchInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Search\Api\SearchInterface|MockObject */ private $search; @@ -61,6 +63,11 @@ class CollectionTest extends BaseCollection */ private $filterBuilder; + /** + * @var SearchResultApplierFactory|MockObject + */ + private $searchResultApplierFactory; + /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection */ @@ -72,7 +79,7 @@ class CollectionTest extends BaseCollection private $filter; /** - * setUp method for CollectionTest + * @inheritdoc */ protected function setUp() { @@ -115,17 +122,10 @@ protected function setUp() ->method('create') ->willReturn($searchCriteriaResolver); - $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) - ->disableOriginalConstructor() - ->setMethods(['apply']) - ->getMockForAbstractClass(); - $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + $this->searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $searchResultApplierFactory->expects($this->any()) - ->method('create') - ->willReturn($searchResultApplier); $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) ->disableOriginalConstructor() @@ -148,7 +148,7 @@ protected function setUp() 'temporaryStorageFactory' => $temporaryStorageFactory, 'productLimitationFactory' => $productLimitationFactoryMock, 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, - 'searchResultApplierFactory' => $searchResultApplierFactory, + 'searchResultApplierFactory' => $this->searchResultApplierFactory, 'totalRecordsResolverFactory' => $totalRecordsResolverFactory, ] ); @@ -161,6 +161,9 @@ protected function setUp() $this->model->setFilterBuilder($this->filterBuilder); } + /** + * @inheritdoc + */ protected function tearDown() { $reflectionProperty = new \ReflectionProperty(\Magento\Framework\App\ObjectManager::class, '_instance'); @@ -168,16 +171,49 @@ protected function tearDown() $reflectionProperty->setValue(null); } + /** + * Test to Return field faceted data from faceted search result + */ public function testGetFacetedDataWithEmptyAggregations() { + $pageSize = 10; + $searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class) ->getMockForAbstractClass(); $this->search->expects($this->once()) ->method('search') ->willReturn($searchResult); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $this->searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $this->model->setPageSize($pageSize); + $this->model->setCurPage(0); + + $this->searchResultApplierFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'collection' => $this->model, + 'searchResult' => $searchResult, + 'orders' => [], + 'size' => $pageSize, + 'currentPage' => 0, + ] + ) + ->willReturn($searchResultApplier); + $this->model->getFacetedData('field'); } + /** + * Test to Apply attribute filter to facet collection + */ public function testAddFieldToFilter() { $this->filter = $this->createFilter(); @@ -220,6 +256,7 @@ protected function getCriteriaBuilder() protected function getFilterBuilder() { $filterBuilder = $this->createMock(\Magento\Framework\Api\FilterBuilder::class); + return $filterBuilder; } @@ -241,6 +278,7 @@ protected function addFiltersToFilterBuilder(MockObject $filterBuilder, array $f ->with($value) ->willReturnSelf(); } + return $filterBuilder; } @@ -252,6 +290,7 @@ protected function createFilter() $filter = $this->getMockBuilder(\Magento\Framework\Api\Filter::class) ->disableOriginalConstructor() ->getMock(); + return $filter; } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php index 8157c1fa8fa82..350344372612a 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogSearch\Test\Unit\Model\Search\RequestGenerator; @@ -11,6 +12,9 @@ use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\FilterInterface; +/** + * Test catalog search range request generator. + */ class DecimalTest extends \PHPUnit\Framework\TestCase { /** @var Decimal */ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php new file mode 100644 index 0000000000000..3635430197591 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Test\Unit\Model\Search\RequestGenerator; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\CatalogSearch\Model\Search\RequestGenerator\Price; +use Magento\Framework\Search\Request\BucketInterface; +use Magento\Framework\Search\Request\FilterInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test catalog search range request generator. + */ +class PriceTest extends \PHPUnit\Framework\TestCase +{ + /** @var Price */ + private $price; + + /** @var Attribute|\PHPUnit_Framework_MockObject_MockObject */ + private $attribute; + + /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $scopeConfigMock; + + protected function setUp() + { + $this->attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->setMethods(['getValue']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->price = $objectManager->getObject( + Price::class, + ['scopeConfig' => $this->scopeConfigMock] + ); + } + + public function testGetFilterData() + { + $filterName = 'test_filter_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attributeCode, + 'from' => '$' . $attributeCode . '.from$', + 'to' => '$' . $attributeCode . '.to$', + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->price->getFilterData($this->attribute, $filterName); + $this->assertEquals($expected, $actual); + } + + public function testGetAggregationData() + { + $bucketName = 'test_bucket_name'; + $attributeCode = 'test_attribute_code'; + $method = 'price_dynamic_algorithm'; + $expected = [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attributeCode, + 'method' => '$'. $method . '$', + 'metric' => [['type' => 'count']], + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->price->getAggregationData($this->attribute, $bucketName); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Ui/DataProvider/Product/AddFulltextFilterToCollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Ui/DataProvider/Product/AddFulltextFilterToCollectionTest.php new file mode 100644 index 0000000000000..881c843ecf92b --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Ui/DataProvider/Product/AddFulltextFilterToCollectionTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Test\Unit\Ui\DataProvider\Product; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\CatalogSearch\Model\ResourceModel\Search\Collection as SearchCollection; +use Magento\Framework\Data\Collection; +use Magento\CatalogSearch\Ui\DataProvider\Product\AddFulltextFilterToCollection; + +class AddFulltextFilterToCollectionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SearchCollection|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchCollection; + + /** + * @var Collection|\PHPUnit_Framework_MockObject_MockObject + */ + private $collection; + + /** + * @var ObjectManagerHelper + */ + private $objectManager; + + /** + * @var AddFulltextFilterToCollection + */ + private $model; + + protected function setUp() + { + $this->objectManager = new ObjectManagerHelper($this); + + $this->searchCollection = $this->getMockBuilder(SearchCollection::class) + ->setMethods(['addBackendSearchFilter', 'load', 'getAllIds']) + ->disableOriginalConstructor() + ->getMock(); + $this->searchCollection->expects($this->any()) + ->method('load') + ->willReturnSelf(); + $this->collection = $this->getMockBuilder(Collection::class) + ->setMethods(['addIdFilter']) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = $this->objectManager->getObject( + AddFulltextFilterToCollection::class, + [ + 'searchCollection' => $this->searchCollection + ] + ); + } + + public function testAddFilter() + { + $this->searchCollection->expects($this->once()) + ->method('addBackendSearchFilter') + ->with('test'); + $this->searchCollection->expects($this->once()) + ->method('getAllIds') + ->willReturn([]); + $this->collection->expects($this->once()) + ->method('addIdFilter') + ->with(-1); + $this->model->addFilter($this->collection, 'test', ['fulltext' => 'test']); + } +} diff --git a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php index f312178e0bf0b..af5020a2f8c94 100644 --- a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php +++ b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php @@ -40,6 +40,10 @@ public function addFilter(Collection $collection, $field, $condition = null) if (isset($condition['fulltext']) && (string)$condition['fulltext'] !== '') { $this->searchCollection->addBackendSearchFilter($condition['fulltext']); $productIds = $this->searchCollection->load()->getAllIds(); + if (empty($productIds)) { + //add dummy id to prevent returning full unfiltered collection + $productIds = -1; + } $collection->addIdFilter($productIds); } } diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 28d5035308dee..4e5b38878ee52 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -75,6 +75,9 @@ <type name="Magento\Catalog\Model\Product\Action"> <plugin name="catalogsearchFulltextMassAction" type="Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Product\Action"/> </type> + <type name="Magento\Catalog\Model\Indexer\Product\Category\Action\Rows"> + <plugin name="catalogsearchFulltextCategoryAssignment" type="Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Product\Category\Action\Rows"/> + </type> <type name="Magento\Store\Model\ResourceModel\Store"> <plugin name="catalogsearchFulltextIndexerStoreView" type="Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Store\View" /> </type> @@ -281,6 +284,7 @@ <argument name="defaultGenerator" xsi:type="object">\Magento\CatalogSearch\Model\Search\RequestGenerator\General</argument> <argument name="generators" xsi:type="array"> <item name="decimal" xsi:type="object">Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal</item> + <item name="price" xsi:type="object">Magento\CatalogSearch\Model\Search\RequestGenerator\Price</item> </argument> </arguments> </type> @@ -373,4 +377,7 @@ </argument> </arguments> </type> + <type name="Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider"> + <plugin name="stockedProductsFilterPlugin" type="Magento\CatalogSearch\Model\Indexer\Plugin\StockedProductsFilterPlugin"/> + </type> </config> diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml index c63e6ff4abe0f..32b26eec9dbe6 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** This changes need to valid applying filters and configuration before search process is started. */ +$productList = $block->getProductListHtml(); ?> <?php if ($block->getResultCount()) : ?> <?= /* @noEscape */ $block->getChildHtml('tagged_product_list_rss_link') ?> @@ -16,7 +19,7 @@ </div> </div> <?php endif; ?> - <?= $block->getProductListHtml() ?> + <?= /* @noEscape */ $productList ?> </div> <?php else : ?> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php index 4a191b54dea68..5d08ea33ff8a1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php @@ -6,6 +6,7 @@ namespace Magento\CatalogUrlRewrite\Model\Product; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Model\ObjectRegistry; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; @@ -67,6 +68,9 @@ public function generate($storeId, Product $product, ObjectRegistry $productCate if ($anchorCategoryIds) { foreach ($anchorCategoryIds as $anchorCategoryId) { $anchorCategory = $this->categoryRepository->get($anchorCategoryId); + if ((int)$anchorCategory->getParentId() === Category::TREE_ROOT_ID) { + continue; + } $urls[] = $this->urlRewriteFactory->create() ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) ->setEntityId($product->getId()) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php index edca633fb14cc..d9e9705ac039d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php @@ -148,7 +148,7 @@ private function getCategoryUrlSuffix($storeId = null): string CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE, $storeId - ); + ) ?? ''; } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 704b60a8aaf2a..b1dfa79373a05 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -23,7 +23,6 @@ use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\ImportExport\Model\Import as ImportExport; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -252,7 +251,7 @@ public function execute(Observer $observer) * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _populateForUrlGeneration($rowData) + private function _populateForUrlGeneration($rowData) { $newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]); $oldSku = $this->import->getOldSku(); @@ -321,7 +320,7 @@ private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): b * @param array $rowData * @return void */ - protected function setStoreToProduct(Product $product, array $rowData) + private function setStoreToProduct(Product $product, array $rowData) { if (!empty($rowData[ImportProduct::COL_STORE]) && ($storeId = $this->import->getStoreIdByCode($rowData[ImportProduct::COL_STORE])) @@ -339,7 +338,7 @@ protected function setStoreToProduct(Product $product, array $rowData) * @param string $storeId * @return $this */ - protected function addProductToImport($product, $storeId) + private function addProductToImport($product, $storeId) { if ($product->getVisibility() == (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]) { return $this; @@ -357,7 +356,7 @@ protected function addProductToImport($product, $storeId) * @param Product $product * @return $this */ - protected function populateGlobalProduct($product) + private function populateGlobalProduct($product) { foreach ($this->import->getProductWebsites($product->getSku()) as $websiteId) { foreach ($this->websitesToStoreIds[$websiteId] as $storeId) { @@ -376,7 +375,7 @@ protected function populateGlobalProduct($product) * @return UrlRewrite[] * @throws LocalizedException */ - protected function generateUrls() + private function generateUrls() { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $mergeDataProvider->merge($this->canonicalUrlRewriteGenerate()); @@ -398,7 +397,7 @@ protected function generateUrls() * @param int|null $storeId * @return bool */ - protected function isGlobalScope($storeId) + private function isGlobalScope($storeId) { return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; } @@ -408,7 +407,7 @@ protected function isGlobalScope($storeId) * * @return UrlRewrite[] */ - protected function canonicalUrlRewriteGenerate() + private function canonicalUrlRewriteGenerate() { $urls = []; foreach ($this->products as $productId => $productsByStores) { @@ -433,7 +432,7 @@ protected function canonicalUrlRewriteGenerate() * @return UrlRewrite[] * @throws LocalizedException */ - protected function categoriesUrlRewriteGenerate() + private function categoriesUrlRewriteGenerate(): array { $urls = []; foreach ($this->products as $productId => $productsByStores) { @@ -444,17 +443,24 @@ protected function categoriesUrlRewriteGenerate() continue; } $requestPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category); - $urls[] = $this->urlRewriteFactory->create() - ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($productId) - ->setRequestPath($requestPath) - ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category)) - ->setStoreId($storeId) - ->setMetadata(['category_id' => $category->getId()]); + $urls[] = [ + $this->urlRewriteFactory->create() + ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($productId) + ->setRequestPath($requestPath) + ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category)) + ->setStoreId($storeId) + ->setMetadata(['category_id' => $category->getId()]) + ]; + $parentCategoryIds = $category->getAnchorsAbove(); + if ($parentCategoryIds) { + $urls[] = $this->getParentCategoriesUrlRewrites($parentCategoryIds, $storeId, $product); + } } } } - return $urls; + $result = !empty($urls) ? array_merge(...$urls) : []; + return $result; } /** @@ -462,7 +468,7 @@ protected function categoriesUrlRewriteGenerate() * * @return UrlRewrite[] */ - protected function currentUrlRewritesRegenerate() + private function currentUrlRewritesRegenerate() { $currentUrlRewrites = $this->urlFinder->findAllByData( [ @@ -496,7 +502,7 @@ protected function currentUrlRewritesRegenerate() * @param Category $category * @return array */ - protected function generateForAutogenerated($url, $category) + private function generateForAutogenerated($url, $category) { $storeId = $url->getStoreId(); $productId = $url->getEntityId(); @@ -532,7 +538,7 @@ protected function generateForAutogenerated($url, $category) * @param Category $category * @return array */ - protected function generateForCustom($url, $category) + private function generateForCustom($url, $category) { $storeId = $url->getStoreId(); $productId = $url->getEntityId(); @@ -566,7 +572,7 @@ protected function generateForCustom($url, $category) * @param UrlRewrite $url * @return Category|null|bool */ - protected function retrieveCategoryFromMetadata($url) + private function retrieveCategoryFromMetadata($url) { $metadata = $url->getMetadata(); if (isset($metadata['category_id'])) { @@ -576,32 +582,6 @@ protected function retrieveCategoryFromMetadata($url) return null; } - /** - * Check, category suited for url-rewrite generation. - * - * @param Category $category - * @param int $storeId - * @return bool - * @throws NoSuchEntityException - */ - protected function isCategoryProperForGenerating($category, $storeId) - { - if (isset($this->acceptableCategories[$storeId]) && - isset($this->acceptableCategories[$storeId][$category->getId()])) { - return $this->acceptableCategories[$storeId][$category->getId()]; - } - $acceptable = false; - if ($category->getParentId() != Category::TREE_ROOT_ID) { - list(, $rootCategoryId) = $category->getParentIds(); - $acceptable = ($rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId()); - } - if (!isset($this->acceptableCategories[$storeId])) { - $this->acceptableCategories[$storeId] = []; - } - $this->acceptableCategories[$storeId][$category->getId()] = $acceptable; - return $acceptable; - } - /** * Get category by id considering store scope. * @@ -635,4 +615,36 @@ private function isCategoryRewritesEnabled() { return (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites'); } + + /** + * Generate url-rewrite for anchor parent-categories. + * + * @param array $categoryIds + * @param int $storeId + * @param Product $product + * @return array + * @throws LocalizedException + */ + private function getParentCategoriesUrlRewrites(array $categoryIds, int $storeId, Product $product): array + { + $urls = []; + foreach ($categoryIds as $categoryId) { + $category = $this->getCategoryById($categoryId, $storeId); + if ($category->getParentId() == Category::TREE_ROOT_ID) { + continue; + } + $requestPath = $this->productUrlPathGenerator + ->getUrlPathWithSuffix($product, $storeId, $category); + $targetPath = $this->productUrlPathGenerator + ->getCanonicalUrlPath($product, $category); + $urls[] = $this->urlRewriteFactory->create() + ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($product->getId()) + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setStoreId($storeId) + ->setMetadata(['category_id' => $category->getId()]); + } + return $urls; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index 7f987124040fd..0d0b0fb995706 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; @@ -18,6 +20,14 @@ */ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface { + + /** + * Reserved endpoint names. + * + * @var string[] + */ + private $invalidValues = []; + /** * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator */ @@ -38,22 +48,34 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface */ private $categoryRepository; + /** + * @var \Magento\Backend\App\Area\FrontNameResolver + */ + private $frontNameResolver; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService * @param CategoryRepositoryInterface $categoryRepository + * @param \Magento\Backend\App\Area\FrontNameResolver $frontNameResolver + * @param string[] $invalidValues */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, ChildrenCategoriesProvider $childrenCategoriesProvider, StoreViewService $storeViewService, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + \Magento\Backend\App\Area\FrontNameResolver $frontNameResolver = null, + array $invalidValues = [] ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->storeViewService = $storeViewService; $this->categoryRepository = $categoryRepository; + $this->frontNameResolver = $frontNameResolver ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Backend\App\Area\FrontNameResolver::class); + $this->invalidValues = $invalidValues; } /** @@ -72,7 +94,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category); $this->updateUrlKey($category, $resultUrlKey); } elseif ($useDefaultAttribute) { - if (!$category->isObjectNew()) { + if (!$category->isObjectNew() && $category->getStoreId() === Store::DEFAULT_STORE_ID) { $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); $this->updateUrlKey($category, $resultUrlKey); } @@ -93,6 +115,17 @@ private function updateUrlKey($category, $urlKey) if (empty($urlKey)) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); } + + if (in_array($urlKey, $this->getInvalidValues())) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'URL key "%1" matches a reserved endpoint name (%2). Use another URL key.', + $urlKey, + implode(', ', $this->getInvalidValues()) + ) + ); + } + $category->setUrlKey($urlKey) ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); if (!$category->isObjectNew()) { @@ -103,6 +136,16 @@ private function updateUrlKey($category, $urlKey) } } + /** + * Get reserved endpoint names. + * + * @return array + */ + private function getInvalidValues() + { + return array_unique(array_merge($this->invalidValues, [$this->frontNameResolver->getFrontName()])); + } + /** * Update url path for children category. * diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index 083b39d621f2a..d1e78897e3269 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -7,8 +7,8 @@ namespace Magento\CatalogUrlRewrite\Observer; -use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; @@ -118,13 +118,15 @@ public function __construct( $this->productCollectionFactory = $productCollectionFactory; $this->categoryBasedProductRewriteGenerator = $categoryBasedProductRewriteGenerator; - $objectManager = ObjectManager::getInstance(); - $mergeDataProviderFactory = $mergeDataProviderFactory ?: $objectManager->get(MergeDataProviderFactory::class); + $mergeDataProviderFactory = $mergeDataProviderFactory + ?? ObjectManager::getInstance()->get(MergeDataProviderFactory::class); $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); - $this->serializer = $serializer ?: $objectManager->get(Json::class); + $this->serializer = $serializer + ?? ObjectManager::getInstance()->get(Json::class); $this->productScopeRewriteGenerator = $productScopeRewriteGenerator - ?: $objectManager->get(ProductScopeRewriteGenerator::class); - $this->scopeConfig = $scopeConfig ?? $objectManager->get(ScopeConfigInterface::class); + ?? ObjectManager::getInstance()->get(ProductScopeRewriteGenerator::class); + $this->scopeConfig = $scopeConfig + ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -207,18 +209,14 @@ public function deleteCategoryRewritesForChildren(Category $category) foreach ($categoryIds as $categoryId) { $this->urlPersist->deleteByData( [ - UrlRewrite::ENTITY_ID => - $categoryId, - UrlRewrite::ENTITY_TYPE => - CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::ENTITY_ID => $categoryId, + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, ] ); $this->urlPersist->deleteByData( [ - UrlRewrite::METADATA => - $this->serializer->serialize(['category_id' => $categoryId]), - UrlRewrite::ENTITY_TYPE => - ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::METADATA => $this->serializer->serialize(['category_id' => $categoryId]), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, ] ); } @@ -252,7 +250,7 @@ private function getCategoryProductsUrlRewrites( ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); - foreach ($productCollection as $product) { + foreach ($this->getProducts($productCollection) as $product) { if (isset($this->isSkippedProduct[$category->getEntityId()]) && in_array($product->getId(), $this->isSkippedProduct[$category->getEntityId()]) ) { @@ -270,6 +268,27 @@ private function getCategoryProductsUrlRewrites( return $mergeDataProvider->getData(); } + /** + * Get products from provided collection + * + * @param Collection $collection + * @return \Generator|Product[] + */ + private function getProducts(Collection $collection): \Generator + { + $collection->setPageSize(1000); + $pageCount = $collection->getLastPageNumber(); + $currentPage = 1; + while ($currentPage <= $pageCount) { + $collection->setCurPage($currentPage); + foreach ($collection as $key => $product) { + yield $key => $product; + } + $collection->clear(); + $currentPage++; + } + } + /** * Generates product URL rewrites. * diff --git a/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php new file mode 100644 index 0000000000000..75f88a8573069 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogUrlRewrite\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Catalog\Setup\CategorySetupFactory; + +/** + * Update url_key to be searchable + */ +class UpdateUrlKeySearchable implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var CategorySetupFactory + */ + private $categorySetupFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param CategorySetupFactory $categorySetupFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + CategorySetupFactory $categorySetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]); + + $categorySetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'url_key', + 'is_searchable', + true + ); + + $categorySetup->updateAttribute( + \Magento\Catalog\Model\Category::ENTITY, + 'url_key', + 'is_searchable', + true + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [CreateUrlAttributes::class]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml new file mode 100644 index 0000000000000..b463b0524d5ff --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml @@ -0,0 +1,17 @@ +<?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="AdminCategoryRestrictedUrlErrorMessage"> + <data key="urlAdmin">URL key "admin" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + <data key="urlSoap">URL key "soap" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + <data key="urlRest">URL key "rest" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + <data key="urlGraphql">URL key "graphql" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml new file mode 100644 index 0000000000000..58b489da5082b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.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="AdminCategoryWithRestrictedUrlKeyNotCreatedTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <stories value="Url rewrites"/> + <title value="Category with restricted Url Key cannot be created"/> + <description value="Category with restricted Url Key cannot be created"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17515"/> + <useCaseId value="MAGETWO-69825"/> + <group value="CatalogUrlRewrite"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created categories--> + <comment userInput="Delete created categories" stepKey="commentDeleteCreatedCategories"/> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteAdminCategory"> + <argument name="categoryName" value="admin"/> + </actionGroup> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteSoapCategory"> + <argument name="categoryName" value="soap"/> + </actionGroup> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteRestCategory"> + <argument name="categoryName" value="rest"/> + </actionGroup> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteGraphQlCategory"> + <argument name="categoryName" value="graphql"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Check category creation with restricted url key 'admin'--> + <comment userInput="Check category creation with restricted url key 'admin'" stepKey="commentCheckAdminCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateAdminCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillAdminFirstCategoryForm"> + <argument name="categoryName" value="admin"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlAdmin}}' stepKey="seeAdminFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillAdminSecondCategoryForm"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + <argument name="categoryUrlKey" value="admin"/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlAdmin}}' stepKey="seeAdminSecondErrorMessage"/> + <!--Create category with 'admin' name--> + <comment userInput="Create category with 'admin' name" stepKey="commentAdminCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillAdminThirdCategoryForm"> + <argument name="categoryName" value="admin"/> + <argument name="categoryUrlKey" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the category." stepKey="seeAdminSuccessMessage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('admin')}}" stepKey="seeAdminCategoryInTree"/> + <!--Check category creation with restricted url key 'soap'--> + <comment userInput="Check category creation with restricted url key 'soap'" stepKey="commentCheckSoapCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateSoapCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillSoapFirstCategoryForm"> + <argument name="categoryName" value="soap"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlSoap}}' stepKey="seeSoapFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillSoapSecondCategoryForm"> + <argument name="categoryName" value="{{ApiCategory.name}}"/> + <argument name="categoryUrlKey" value="soap"/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlSoap}}' stepKey="seeSoapSecondErrorMessage"/> + <!--Create category with 'soap' name--> + <comment userInput="Create category with 'soap' name" stepKey="commentSoapCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillSoapThirdCategoryForm"> + <argument name="categoryName" value="soap"/> + <argument name="categoryUrlKey" value="{{ApiCategory.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the category." stepKey="seeSoapSuccessMessage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('soap')}}" stepKey="seeSoapCategoryInTree"/> + <!--Check category creation with restricted url key 'rest'--> + <comment userInput="Check category creation with restricted url key 'rest'" stepKey="commentCheckRestCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateRestCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillRestFirstCategoryForm"> + <argument name="categoryName" value="rest"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlRest}}' stepKey="seeRestFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillRestSecondCategoryForm"> + <argument name="categoryName" value="{{SubCategoryWithParent.name}}"/> + <argument name="categoryUrlKey" value="rest"/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlRest}}' stepKey="seeRestSecondErrorMessage"/> + <!--Create category with 'rest' name--> + <comment userInput="Create category with 'rest' name" stepKey="commentRestCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillRestThirdCategoryForm"> + <argument name="categoryName" value="rest"/> + <argument name="categoryUrlKey" value="{{SubCategoryWithParent.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the category." stepKey="seeRestSuccessMesdgssage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('rest')}}" stepKey="seeRestCategoryInTree"/> + <!--Check category creation with restricted url key 'graphql'--> + <comment userInput="Check category creation with restricted url key 'graphql'" stepKey="commentCheckGraphQlCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillGraphQlFirstCategoryForm"> + <argument name="categoryName" value="graphql"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlGraphql}}' stepKey="seeGraphQlFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillGraphQlSecondCategoryForm"> + <argument name="categoryName" value="{{NewSubCategoryWithParent.name}}"/> + <argument name="categoryUrlKey" value="graphql"/> + </actionGroup> + <see selector="{{AdminMessagesSection.error}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlGraphql}}' stepKey="seeGraphQlSecondErrorMessage"/> + <!--Create category with 'graphql' name--> + <comment userInput="Create category with 'graphql' name" stepKey="commentGraphQlCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillGraphQlThirdCategoryForm"> + <argument name="categoryName" value="graphql"/> + <argument name="categoryUrlKey" value="{{NewSubCategoryWithParent.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the category." stepKey="seeGraphQlSuccessMessage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('graphql')}}" stepKey="seeGraphQlCategoryInTree"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/AnchorUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/AnchorUrlRewriteGeneratorTest.php new file mode 100644 index 0000000000000..662e156b8f100 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/AnchorUrlRewriteGeneratorTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Product; + +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class AnchorUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase +{ + /** @var \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator */ + protected $anchorUrlRewriteGenerator; + + /** @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ + protected $productUrlPathGenerator; + + /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ + protected $product; + + /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $categoryRepositoryInterface; + + /** @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry|\PHPUnit_Framework_MockObject_MockObject */ + protected $categoryRegistry; + + /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory|\PHPUnit_Framework_MockObject_MockObject */ + protected $urlRewriteFactory; + + /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|\PHPUnit_Framework_MockObject_MockObject */ + protected $urlRewrite; + + protected function setUp() + { + $this->urlRewriteFactory = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor()->getMock(); + $this->urlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) + ->disableOriginalConstructor()->getMock(); + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor()->getMock(); + $this->categoryRepositoryInterface = $this->getMockBuilder( + \Magento\Catalog\Api\CategoryRepositoryInterface::class + )->disableOriginalConstructor()->getMock(); + $this->categoryRegistry = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class) + ->disableOriginalConstructor()->getMock(); + $this->productUrlPathGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class + )->disableOriginalConstructor()->getMock(); + $this->anchorUrlRewriteGenerator = (new ObjectManager($this))->getObject( + \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::class, + [ + 'productUrlPathGenerator' => $this->productUrlPathGenerator, + 'urlRewriteFactory' => $this->urlRewriteFactory, + 'categoryRepository' => $this->categoryRepositoryInterface + ] + ); + } + + public function testGenerateEmpty() + { + $this->categoryRegistry->expects($this->any())->method('getList')->will($this->returnValue([])); + + $this->assertEquals( + [], + $this->anchorUrlRewriteGenerator->generate(1, $this->product, $this->categoryRegistry) + ); + } + + public function testGenerateCategories() + { + $urlPathWithCategory = 'category1/category2/category3/simple-product.html'; + $storeId = 10; + $productId = 12; + $canonicalUrlPathWithCategory = 'canonical-path-with-category'; + $categoryParentId = '1'; + $categoryIds = [$categoryParentId,'2','3','4']; + $urls = ['category1/simple-product.html', + 'category1/category2/simple-product.html', + 'category1/category2/category3/simple-product.html']; + + $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId)); + $this->productUrlPathGenerator->expects($this->any())->method('getUrlPathWithSuffix') + ->will($this->returnValue($urlPathWithCategory)); + $this->productUrlPathGenerator->expects($this->any())->method('getCanonicalUrlPath') + ->will($this->returnValue($canonicalUrlPathWithCategory)); + $category = $this->createMock(\Magento\Catalog\Model\Category::class); + $category->expects($this->any())->method('getId')->will($this->returnValue($categoryIds)); + $category->expects($this->any())->method('getAnchorsAbove')->will($this->returnValue($categoryIds)); + $category->expects($this->any())->method('getParentId')->will( + $this->onConsecutiveCalls( + $categoryIds[0], + $categoryIds[1], + $categoryIds[2], + $categoryIds[3] + ) + ); + $this->categoryRepositoryInterface + ->expects($this->any()) + ->method('get') + ->withConsecutive( + [ 'category_id' => $categoryIds[0]], + [ 'category_id' => $categoryIds[1]], + [ 'category_id' => $categoryIds[2]] + ) + ->will($this->returnValue($category)); + $this->categoryRegistry->expects($this->any())->method('getList') + ->will($this->returnValue([$category])); + $this->urlRewrite->expects($this->any())->method('setStoreId') + ->with($storeId) + ->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('setEntityId') + ->with($productId) + ->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('setEntityType') + ->with(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('setRequestPath') + ->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('setTargetPath') + ->will($this->returnSelf()); + $this->urlRewrite->expects($this->any())->method('setMetadata') + ->will( + $this->onConsecutiveCalls( + $urls[0], + $urls[1], + $urls[2] + ) + ); + $this->urlRewriteFactory->expects($this->any())->method('create')->will( + $this->returnValue($this->urlRewrite) + ); + + $this->assertEquals( + $urls, + $this->anchorUrlRewriteGenerator->generate($storeId, $this->product, $this->categoryRegistry) + ); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index 3984d949332d3..94fe6ae8c54dc 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -153,24 +153,32 @@ class AfterImportDataObserverTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->importProduct = $this->createPartialMock(\Magento\CatalogImportExport\Model\Import\Product::class, [ + $this->importProduct = $this->createPartialMock( + \Magento\CatalogImportExport\Model\Import\Product::class, + [ 'getNewSku', 'getProductCategories', 'getProductWebsites', 'getStoreIdByCode', 'getCategoryProcessor', - ]); - $this->catalogProductFactory = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, [ + ] + ); + $this->catalogProductFactory = $this->createPartialMock( + \Magento\Catalog\Model\ProductFactory::class, + [ 'create', - ]); + ] + ); $this->storeManager = $this ->getMockBuilder( \Magento\Store\Model\StoreManagerInterface::class ) ->disableOriginalConstructor() - ->setMethods([ - 'getWebsite', - ]) + ->setMethods( + [ + 'getWebsite', + ] + ) ->getMockForAbstractClass(); $this->event = $this->createPartialMock(\Magento\Framework\Event::class, ['getAdapter', 'getBunch']); $this->event->expects($this->any())->method('getAdapter')->willReturn($this->importProduct); @@ -202,9 +210,11 @@ protected function setUp() ); $this->urlFinder = $this ->getMockBuilder(\Magento\UrlRewrite\Model\UrlFinderInterface::class) - ->setMethods([ - 'findAllByData', - ]) + ->setMethods( + [ + 'findAllByData', + ] + ) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -269,9 +279,12 @@ public function testAfterImportData() $newSku = [['entity_id' => 'value'], ['entity_id' => 'value3']]; $websiteId = 'websiteId value'; $productsCount = count($this->products); - $websiteMock = $this->createPartialMock(\Magento\Store\Model\Website::class, [ + $websiteMock = $this->createPartialMock( + \Magento\Store\Model\Website::class, + [ 'getStoreIds', - ]); + ] + ); $storeIds = [1, Store::DEFAULT_STORE_ID]; $websiteMock ->expects($this->once()) @@ -315,13 +328,16 @@ public function testAfterImportData() ->expects($this->exactly(1)) ->method('getStoreIdByCode') ->will($this->returnValueMap($map)); - $product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + $product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ 'getId', 'setId', 'getSku', 'setStoreId', 'getStoreId', - ]); + ] + ); $product ->expects($this->exactly($productsCount)) ->method('setId') @@ -341,17 +357,21 @@ public function testAfterImportData() $product ->expects($this->exactly($productsCount)) ->method('getSku') - ->will($this->onConsecutiveCalls( - $this->products[0]['sku'], - $this->products[1]['sku'] - )); + ->will( + $this->onConsecutiveCalls( + $this->products[0]['sku'], + $this->products[1]['sku'] + ) + ); $product ->expects($this->exactly($productsCount)) ->method('getStoreId') - ->will($this->onConsecutiveCalls( - $this->products[0][ImportProduct::COL_STORE], - $this->products[1][ImportProduct::COL_STORE] - )); + ->will( + $this->onConsecutiveCalls( + $this->products[0][ImportProduct::COL_STORE], + $this->products[1][ImportProduct::COL_STORE] + ) + ); $product ->expects($this->exactly($productsCount)) ->method('setStoreId') @@ -540,7 +560,10 @@ public function testCategoriesUrlRewriteGenerate() ->expects($this->any()) ->method('getId') ->will($this->returnValue($this->categoryId)); - + $category + ->expects($this->any()) + ->method('getAnchorsAbove') + ->willReturn([]); $categoryCollection = $this->getMockBuilder(CategoryCollection::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index 0a570adab309a..9e2090e36f08e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -159,6 +159,9 @@ public function testShouldThrowExceptionIfUrlKeyIsEmpty($useDefaultUrlKey, $isOb $this->expectExceptionMessage('Invalid URL key'); $categoryData = ['use_default' => ['url_key' => $useDefaultUrlKey], 'url_key' => '', 'url_path' => '']; $this->category->setData($categoryData); + $this->category + ->method('getStoreId') + ->willReturn(\Magento\Store\Model\Store::DEFAULT_STORE_ID); $this->category->isObjectNew($isObjectNew); $this->assertEquals($isObjectNew, $this->category->isObjectNew()); $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); diff --git a/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php b/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php index bcb5154e35501..10791eae5405f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php +++ b/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php @@ -53,7 +53,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -65,7 +65,7 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -95,16 +95,21 @@ protected function addUrlRewriteCheckbox(array $meta) ScopeInterface::SCOPE_STORE, $this->locator->getProduct()->getStoreId() ); - - $meta = $this->arrayManager->merge($containerPath, $meta, [ - 'arguments' => [ - 'data' => [ - 'config' => [ - 'component' => 'Magento_Ui/js/form/components/group', + $meta = $this->arrayManager->merge( + $containerPath, + $meta, + [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'component' => 'Magento_Ui/js/form/components/group', + 'label' => false, + 'required' => false, + ], ], ], - ], - ]); + ] + ); $checkbox['arguments']['data']['config'] = [ 'componentType' => Field::NAME, diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index e6fbcaefd0768..5fb7d33546d60 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -45,4 +45,29 @@ </argument> </arguments> </type> + <type name="Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver"> + <arguments> + <argument name="invalidValues" xsi:type="array"> + <item name="0" xsi:type="string">admin</item> + <item name="1" xsi:type="string">soap</item> + <item name="2" xsi:type="string">rest</item> + <item name="3" xsi:type="string">graphql</item> + <item name="4" xsi:type="string">standard</item> + </argument> + </arguments> + </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="url_key" xsi:type="string">catalog_product</item> + <item name="url_path" xsi:type="string">catalog_product</item> + </item> + <item name="catalog_category" xsi:type="array"> + <item name="url_key" xsi:type="string">catalog_category</item> + <item name="url_path" xsi:type="string">catalog_category</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv index b3335dc3523ca..0f21e8ddf9fc9 100644 --- a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv +++ b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv @@ -5,4 +5,5 @@ "Product URL Suffix","Product URL Suffix" "Use Categories Path for Product URLs","Use Categories Path for Product URLs" "Create Permanent Redirect for URLs if URL Key Changed","Create Permanent Redirect for URLs if URL Key Changed" -"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites" \ No newline at end of file +"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites" +"URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key.","URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key." diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php new file mode 100644 index 0000000000000..59708d90c23b7 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Returns the url suffix for category + */ +class CategoryUrlSuffix implements ResolverInterface +{ + /** + * System setting for the url suffix for categories + * + * @var string + */ + private static $xml_path_category_url_suffix = 'catalog/seo/category_url_suffix'; + + /** + * Cache for product rewrite suffix + * + * @var array + */ + private $categoryUrlSuffix = []; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): string { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->getCategoryUrlSuffix($storeId); + } + + /** + * Retrieve category url suffix by store + * + * @param int $storeId + * @return string + */ + private function getCategoryUrlSuffix(int $storeId): string + { + if (!isset($this->categoryUrlSuffix[$storeId])) { + $this->categoryUrlSuffix[$storeId] = $this->scopeConfig->getValue( + self::$xml_path_category_url_suffix, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } + return $this->categoryUrlSuffix[$storeId]; + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php new file mode 100644 index 0000000000000..9a0193ba36367 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Returns the url suffix for product + */ +class ProductUrlSuffix implements ResolverInterface +{ + /** + * System setting for the url suffix for products + * + * @var string + */ + private static $xml_path_product_url_suffix = 'catalog/seo/product_url_suffix'; + + /** + * Cache for product rewrite suffix + * + * @var array + */ + private $productUrlSuffix = []; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): string { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->getProductUrlSuffix($storeId); + } + + /** + * Retrieve product url suffix by store + * + * @param int $storeId + * @return string + */ + private function getProductUrlSuffix(int $storeId): string + { + if (!isset($this->productUrlSuffix[$storeId])) { + $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( + self::$xml_path_product_url_suffix, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } + return $this->productUrlSuffix[$storeId]; + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json index e276da0cc6fd8..202c573c2ae04 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json @@ -4,6 +4,7 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/module-store": "*", "magento/module-catalog": "*", "magento/framework": "*" }, diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml index 20e6b7e9c0053..8724972e71b17 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml @@ -14,4 +14,20 @@ </argument> </arguments> </type> + + <type name="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader"> + <arguments> + <argument name="exactMatchAttributes" xsi:type="array"> + <item name="url_key" xsi:type="string">url_key</item> + </argument> + </arguments> + </type> + + <type name="Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite"> + <arguments> + <argument name="entityTypeMapping" xsi:type="array"> + <item name="catalog_product" xsi:type="const">Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls index 89108e578d673..82facf6959f3c 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls @@ -3,15 +3,24 @@ interface ProductInterface { url_key: String @doc(description: "The part of the URL that identifies the product") + url_suffix: String @doc(description: "The part of the product URL that is appended after the url key") @resolver(class: "Magento\\CatalogUrlRewriteGraphQl\\Model\\Resolver\\ProductUrlSuffix") url_path: String @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") url_rewrites: [UrlRewrite] @doc(description: "URL rewrites list") @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") } +interface CategoryInterface { + url_suffix: String @doc(description: "The part of the category URL that is appended after the url key") @resolver(class: "Magento\\CatalogUrlRewriteGraphQl\\Model\\Resolver\\CategoryUrlSuffix") +} + input ProductFilterInput { url_key: FilterTypeInput @doc(description: "The part of the URL that identifies the product") url_path: FilterTypeInput @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") } +input ProductAttributeFilterInput { + url_key: FilterEqualTypeInput @doc(description: "The part of the URL that identifies the product") +} + input ProductSortInput { url_key: SortEnum @doc(description: "The part of the URL that identifies the product") url_path: SortEnum @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index e5fb20a58aea1..a712ae91cbfa9 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -11,6 +11,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ProductCategoryList; +use Magento\Store\Model\Store; /** * Class Product @@ -106,7 +107,7 @@ function ($attribute) { } /** - * {@inheritdoc} + * @inheritdoc * * @param array &$attributes * @return void @@ -164,6 +165,8 @@ public function addToCollection($collection) } /** + * Adds Attributes that belong to Global Scope + * * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection * @return $this @@ -200,6 +203,8 @@ protected function addGlobalAttribute( } /** + * Adds Attributes that don't belong to Global Scope + * * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection * @return $this @@ -208,7 +213,7 @@ protected function addNotGlobalAttribute( \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute, \Magento\Catalog\Model\ResourceModel\Product\Collection $collection ) { - $storeId = $this->storeManager->getStore()->getId(); + $storeId = $this->storeManager->getStore()->getId(); $values = $collection->getAllAttributeValues($attribute); $validEntities = []; if ($values) { @@ -218,7 +223,9 @@ protected function addNotGlobalAttribute( $validEntities[] = $entityId; } } else { - if ($this->validateAttribute($storeValues[\Magento\Store\Model\Store::DEFAULT_STORE_ID])) { + if (isset($storeValues[Store::DEFAULT_STORE_ID]) && + $this->validateAttribute($storeValues[Store::DEFAULT_STORE_ID]) + ) { $validEntities[] = $entityId; } } @@ -236,7 +243,7 @@ protected function addNotGlobalAttribute( } /** - * {@inheritdoc} + * @inheritdoc * * @return string */ @@ -257,7 +264,7 @@ public function getMappedSqlField() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection * @return $this diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 6722d0df93752..8c1bd220a0f32 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -14,7 +14,8 @@ "magento/module-rule": "*", "magento/module-store": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php index 4941bf8451bf8..c99c9041941b1 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php @@ -85,7 +85,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements protected $priceCurrency; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ public $moduleManager; @@ -105,7 +105,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param PriceCurrencyInterface $priceCurrency - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param InterpretationStrategyInterface $messageInterpretationStrategy * @param array $data * @param ItemResolverInterface|null $itemResolver @@ -120,7 +120,7 @@ public function __construct( \Magento\Framework\Url\Helper\Data $urlHelper, \Magento\Framework\Message\ManagerInterface $messageManager, PriceCurrencyInterface $priceCurrency, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, InterpretationStrategyInterface $messageInterpretationStrategy, array $data = [], ItemResolverInterface $itemResolver = null diff --git a/app/code/Magento/Checkout/Block/Cart/Link.php b/app/code/Magento/Checkout/Block/Cart/Link.php index 6ea5137521106..9e6db1754d9e4 100644 --- a/app/code/Magento/Checkout/Block/Cart/Link.php +++ b/app/code/Magento/Checkout/Block/Cart/Link.php @@ -13,7 +13,7 @@ class Link extends \Magento\Framework\View\Element\Html\Link { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; @@ -24,14 +24,14 @@ class Link extends \Magento\Framework\View\Element\Html\Link /** * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Checkout\Helper\Cart $cartHelper * @param array $data * @codeCoverageIgnore */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Checkout\Helper\Cart $cartHelper, array $data = [] ) { diff --git a/app/code/Magento/Checkout/Block/Link.php b/app/code/Magento/Checkout/Block/Link.php index 3d0740181f4a5..4ab2981e9185e 100644 --- a/app/code/Magento/Checkout/Block/Link.php +++ b/app/code/Magento/Checkout/Block/Link.php @@ -13,7 +13,7 @@ class Link extends \Magento\Framework\View\Element\Html\Link { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; @@ -24,14 +24,14 @@ class Link extends \Magento\Framework\View\Element\Html\Link /** * @param \Magento\Framework\View\Element\Template\Context $context - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Checkout\Helper\Data $checkoutHelper * @param array $data * @codeCoverageIgnore */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Checkout\Helper\Data $checkoutHelper, array $data = [] ) { diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php index ac4a93e6066a4..9d17e32b2c93d 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php @@ -8,16 +8,25 @@ namespace Magento\Checkout\Controller\Cart; use Magento\Checkout\Model\Cart\RequestQuantityProcessor; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Exception\LocalizedException; -use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Quote\Model\Quote\Item; use Psr\Log\LoggerInterface; -class UpdateItemQty extends \Magento\Framework\App\Action\Action +/** + * UpdateItemQty ajax request + * + * @package Magento\Checkout\Controller\Cart + */ +class UpdateItemQty extends Action implements HttpPostActionInterface { + /** * @var RequestQuantityProcessor */ @@ -44,13 +53,16 @@ class UpdateItemQty extends \Magento\Framework\App\Action\Action private $logger; /** - * @param Context $context, + * UpdateItemQty constructor + * + * @param Context $context * @param RequestQuantityProcessor $quantityProcessor * @param FormKeyValidator $formKeyValidator * @param CheckoutSession $checkoutSession * @param Json $json * @param LoggerInterface $logger */ + public function __construct( Context $context, RequestQuantityProcessor $quantityProcessor, @@ -68,30 +80,26 @@ public function __construct( } /** + * Controller execute method + * * @return void */ public function execute() { try { - if (!$this->formKeyValidator->validate($this->getRequest())) { - throw new LocalizedException( - __('Something went wrong while saving the page. Please refresh the page and try again.') - ); - } + $this->validateRequest(); + $this->validateFormKey(); $cartData = $this->getRequest()->getParam('cart'); - if (!is_array($cartData)) { - throw new LocalizedException( - __('Something went wrong while saving the page. Please refresh the page and try again.') - ); - } + + $this->validateCartData($cartData); $cartData = $this->quantityProcessor->process($cartData); $quote = $this->checkoutSession->getQuote(); foreach ($cartData as $itemId => $itemInfo) { $item = $quote->getItemById($itemId); - $qty = isset($itemInfo['qty']) ? (double)$itemInfo['qty'] : 0; + $qty = isset($itemInfo['qty']) ? (double) $itemInfo['qty'] : 0; if ($item) { $this->updateItemQuantity($item, $qty); } @@ -111,11 +119,13 @@ public function execute() * * @param Item $item * @param float $qty + * @return void * @throws LocalizedException */ private function updateItemQuantity(Item $item, float $qty) { if ($qty > 0) { + $item->clearMessage(); $item->setQty($qty); if ($item->getHasError()) { @@ -145,9 +155,7 @@ private function jsonResponse(string $error = '') */ private function getResponseData(string $error = ''): array { - $response = [ - 'success' => true, - ]; + $response = ['success' => true]; if (!empty($error)) { $response = [ @@ -158,4 +166,48 @@ private function getResponseData(string $error = ''): array return $response; } + + /** + * Validates the Request HTTP method + * + * @return void + * @throws NotFoundException + */ + private function validateRequest() + { + if ($this->getRequest()->isPost() === false) { + throw new NotFoundException(__('Page Not Found')); + } + } + + /** + * Validates form key + * + * @return void + * @throws LocalizedException + */ + private function validateFormKey() + { + if (!$this->formKeyValidator->validate($this->getRequest())) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + } + + /** + * Validates cart data + * + * @param array|null $cartData + * @return void + * @throws LocalizedException + */ + private function validateCartData($cartData = null) + { + if (!is_array($cartData)) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + } } diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php index 70352b50d8de4..fdf49d6765a29 100644 --- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php +++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php @@ -397,6 +397,9 @@ private function getQuoteData() if ($this->checkoutSession->getQuote()->getId()) { $quote = $this->quoteRepository->get($this->checkoutSession->getQuote()->getId()); $quoteData = $quote->toArray(); + if (null !== $quote->getExtensionAttributes()) { + $quoteData['extension_attributes'] = $quote->getExtensionAttributes()->__toArray(); + } $quoteData['is_virtual'] = $quote->getIsVirtual(); if (!$quote->getCustomer()->getId()) { diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index da29482f0123f..cae78389d4120 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -193,7 +193,9 @@ private function limitShippingCarrier(Quote $quote) : void $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); - $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); + if ($shippingRate) { + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); + } } } } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 2eced5c642261..2f1a36318ebc8 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -124,9 +124,9 @@ public function savePaymentInformation( $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); - $shippingAddress->setLimitCarrier( - $shippingRate ? $shippingRate->getCarrier() : $shippingAddress->getShippingMethod() - ); + if ($shippingRate) { + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); + } } } $this->paymentMethodManagement->set($cartId, $paymentMethod); diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index a654c78853d7a..4a4861fa9ccd2 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -7,6 +7,8 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteIdMaskFactory; use Psr\Log\LoggerInterface; @@ -21,9 +23,6 @@ */ class Session extends \Magento\Framework\Session\SessionManager { - /** - * Checkout state begin - */ const CHECKOUT_STATE_BEGIN = 'begin'; /** @@ -228,7 +227,7 @@ public function setLoadInactive($load = true) * * @return Quote * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -273,21 +272,17 @@ public function getQuote() */ $quote = $this->quoteRepository->get($this->getQuoteId()); } - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { $this->setQuoteId(null); } } if (!$this->getQuoteId()) { if ($this->_customerSession->isLoggedIn() || $this->_customer) { - $customerId = $this->_customer - ? $this->_customer->getId() - : $this->_customerSession->getCustomerId(); - try { - $quote = $this->quoteRepository->getActiveForCustomer($customerId); - $this->setQuoteId($quote->getId()); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - $this->logger->critical($e); + $quoteByCustomer = $this->getQuoteByCustomer(); + if ($quoteByCustomer !== null) { + $this->setQuoteId($quoteByCustomer->getId()); + $quote = $quoteByCustomer; } } else { $quote->setIsCheckoutCart(true); @@ -375,7 +370,7 @@ public function loadCustomerQuote() try { $customerQuote = $this->quoteRepository->getForCustomer($this->_customerSession->getCustomerId()); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { $customerQuote = $this->quoteFactory->create(); } $customerQuote->setStoreId($this->_storeManager->getStore()->getId()); @@ -558,7 +553,7 @@ public function restoreQuote() $this->replaceQuote($quote)->unsLastRealOrderId(); $this->_eventManager->dispatch('restore_quote', ['order' => $order, 'quote' => $quote]); return true; - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { $this->logger->critical($e); } } @@ -588,4 +583,22 @@ protected function isQuoteMasked() { return $this->isQuoteMasked; } + + /** + * Returns quote for customer if there is any + */ + private function getQuoteByCustomer(): ?CartInterface + { + $customerId = $this->_customer + ? $this->_customer->getId() + : $this->_customerSession->getCustomerId(); + + try { + $quote = $this->quoteRepository->getActiveForCustomer($customerId); + } catch (NoSuchEntityException $e) { + $quote = null; + } + + return $quote; + } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertIsNotVisibleCartPagerTextActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertIsNotVisibleCartPagerTextActionGroup.xml new file mode 100644 index 0000000000000..2072cb6df1dc1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertIsNotVisibleCartPagerTextActionGroup.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="AssertPagerTextIsNotVisibleActionGroup"> + <arguments> + <argument name="text" type="string"/> + </arguments> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + <dontSee userInput="{{text}}" selector="{{StorefrontCartToolbarSection.toolbarNumber}}" stepKey="VerifyMissingPagerText"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml new file mode 100644 index 0000000000000..9ff7e5a96fae7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml @@ -0,0 +1,20 @@ +<?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="AssertStorefrontCheckoutSuccessActionGroup"> + <annotations> + <description>Verifies if the order is placed successfully on the 'one page checkout' page.</description> + </annotations> + <waitForElement selector="{{CheckoutSuccessMainSection.successTitle}}" stepKey="waitForLoadSuccessPageTitle"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="seeOrderLink"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml index 176eebed142c8..8933ebbc1dd84 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml @@ -18,12 +18,24 @@ <argument name="cartSubtotal" type="string"/> <argument name="qty" type="string"/> </arguments> - - <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeProductNameInMiniCart"/> - <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productPrice}}" stepKey="seeProductPriceInMiniCart"/> + + <see selector="{{StorefrontMinicartSection.productPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="seeProductPriceInMiniCart"/> <seeElement selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="seeCheckOutButtonInMiniCart"/> <seeElement selector="{{StorefrontMinicartSection.productQuantity(productName, qty)}}" stepKey="seeProductQuantity1"/> <seeElement selector="{{StorefrontMinicartSection.productImage}}" stepKey="seeProductImage"/> <see selector="{{StorefrontMinicartSection.productSubTotal}}" userInput="{{cartSubtotal}}" stepKey="seeSubTotal"/> </actionGroup> + + <actionGroup name="AssertStorefrontMiniCartProductDetailsAbsentActionGroup"> + <annotations> + <description>Validates that the provided Product details (Name, Price) are + not present in the Storefront Mini Shopping Cart.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + <argument name="productPrice" type="string"/> + </arguments> + + <dontSee selector="{{StorefrontMinicartSection.productPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="dontSeeProductPriceInMiniCart"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml new file mode 100644 index 0000000000000..1ec42033a782b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup"> + <annotations> + <description>Validates value of the Shipping total is not calculated.</description> + </annotations> + + <arguments> + <argument name="value" defaultValue="Not yet calculated" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" time="30" stepKey="waitForShippingTotalToBeVisible"/> + <see selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" userInput="{{value}}" stepKey="assertShippingTotalIsNotYetCalculated"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml new file mode 100644 index 0000000000000..4f9555d84898d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AssertStorefrontOrderCannotBePlacedActionGroup"> + <annotations> + <description>Validates order cannot be placed and checks error message.</description> + </annotations> + + <arguments> + <argument name="error" type="string"/> + </arguments> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrderWithoutTimeout}}" stepKey="clickPlaceOrder"/> + <waitForElement selector="{{CheckoutCartMessageSection.errorMessage}}" time="30" stepKey="waitForErrorMessage"/> + <see selector="{{CheckoutCartMessageSection.errorMessage}}" userInput="{{error}}" stepKey="assertErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup.xml new file mode 100644 index 0000000000000..6a8efdb507c3e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup"> + <annotations> + <description>Validates that the Shipping label description is present and correct.</description> + </annotations> + + <arguments> + <argument name="labelDescription" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutOrderSummarySection.orderSummaryShippingTotalLabelDescription}}" time="30" stepKey="waitForElement"/> + <see selector="{{CheckoutOrderSummarySection.orderSummaryShippingTotalLabelDescription}}" userInput="{{labelDescription}}" stepKey="seeShippingMethodLabelDescription"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml index e74f5c24fb4f6..fe5887bbf6f7c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml @@ -16,9 +16,7 @@ <argument name="shipping" type="string"/> </arguments> - <waitForElementVisible selector="{{CheckoutCartSummarySection.shipping}}" stepKey="waitForElementToBeVisible" after="assertSubtotal"/> - <reloadPage stepKey="reloadPage" after="waitForElementToBeVisible" /> - <waitForPageLoad after="reloadPage" stepKey="WaitForPageLoaded" /> - <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="30" stepKey="assertShipping" after="WaitForPageLoaded"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.shipping}}" time="60" after="assertSubtotal" stepKey="waitForShippingElementToBeVisible"/> + <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="30" after="waitForShippingElementToBeVisible" stepKey="assertShipping"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertToolbarTextIsVisibleInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertToolbarTextIsVisibleInCartActionGroup.xml new file mode 100644 index 0000000000000..6ede2a0dd5388 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertToolbarTextIsVisibleInCartActionGroup.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="AssertToolbarTextIsVisibleInCartActionGroup"> + <arguments> + <argument name="text" type="string"/> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="{{text}}" selector="{{StorefrontCartToolbarSection.toolbarNumber}}" stepKey="VerifyPageText"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index 7f6980d0c9744..4c7d4e31b2d6f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -64,6 +64,7 @@ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="waitForShippingMethod"/> <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> @@ -170,6 +171,15 @@ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> + <!-- Submit Shipping Address on Checkout Shipping page --> + <actionGroup name="StorefrontCheckoutForwardFromShippingStep"> + <annotations> + <description>Clicks next on Checkout Shipping step</description> + </annotations> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + </actionGroup> + <!-- Logged in user checkout filling shipping section --> <actionGroup name="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup"> <annotations> @@ -221,7 +231,7 @@ <!-- Check product in checkout cart items --> <actionGroup name="CheckProductInCheckoutCartItemsActionGroup"> <annotations> - <description>Validates the the provided Product appears in the Storefront Checkout 'Order Summary' section.</description> + <description>Validates the provided Product appears in the Storefront Checkout 'Order Summary' section.</description> </annotations> <arguments> <argument name="productVar"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml index 789a61a1700db..77734cc75497f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -28,6 +28,10 @@ <fillField selector="{{CheckoutPaymentSection.guestPostcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutPaymentSection.guestTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> </actionGroup> + <actionGroup name="StorefrontCheckoutFillNewBillingAddressActionGroup" extends="GuestCheckoutFillNewBillingAddressActionGroup"> + <remove keyForRemoval="enterEmail"/> + <remove keyForRemoval="waitForLoading3"/> + </actionGroup> <actionGroup name="LoggedInCheckoutFillNewBillingAddressActionGroup"> <annotations> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml index 112abfbb5897a..9f766742b545f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml @@ -17,13 +17,12 @@ <argument name="total" type="string"/> <argument name="discount" type="string"/> </arguments> - <waitForPageLoad stepKey="waitForPageLoad"/> - <waitForLoadingMaskToDisappear stepKey="waitForPrices"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <waitForElementVisible time="30" selector="{{CheckoutCartSummarySection.total}}" stepKey="waitForTotalElement"/> + <see selector="{{CheckoutCartSummarySection.total}}" userInput="${{total}}" stepKey="assertTotal"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-${{discount}}" stepKey="assertDiscount"/> - <wait time="10" stepKey="waitForTotalPrice"/> - <reloadPage stepKey="reloadPage" after="waitForTotalPrice" /> - <waitForPageLoad after="reloadPage" stepKey="WaitForPageLoaded" /> - <see selector="{{CheckoutCartSummarySection.total}}" userInput="${{total}}" stepKey="assertTotal" after="WaitForPageLoaded"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartPageActionGroup.xml new file mode 100644 index 0000000000000..fe1e48e00c5bb --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartPageActionGroup.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="StorefrontOpenCartPageActionGroup"> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="openCartPage" /> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml index c0a160fcb2a71..b07bcdccce674 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -103,7 +103,7 @@ <arguments> <argument name="subtotal" type="string"/> <argument name="shipping" type="string"/> - <argument name="shippingMethod" type="string"/> + <argument name="shippingMethod" type="string" defaultValue="Flat Rate - Fixed"/> <argument name="total" type="string"/> </arguments> @@ -112,6 +112,7 @@ <conditionalClick selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.shippingMethodForm}}" visible="false" stepKey="openEstimateShippingSection"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="waitForShippingSection"/> <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectShippingMethod"/> + <scrollTo selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="scrollToSummary"/> <see userInput="{{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> <see userInput="({{shippingMethod}})" selector="{{CheckoutCartSummarySection.shippingMethod}}" stepKey="assertShippingMethod"/> <reloadPage stepKey="reloadPage" after="assertShippingMethod" /> @@ -146,4 +147,15 @@ <fillField stepKey="fillZip" selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{taxCode.zip}}"/> <waitForPageLoad stepKey="waitForFormUpdate"/> </actionGroup> + + <actionGroup name="StorefrontCheckCartTotalWithDiscountCategoryActionGroup" extends="StorefrontCheckCartActionGroup"> + <annotations> + <description>EXTENDS: StorefrontCheckCartActionGroup. Validates that the provided Discount is present in the Storefront Shopping Cart.</description> + </annotations> + <arguments> + <argument name="discount" type="string" defaultValue="0"/> + </arguments> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscount"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-${{discount}}" stepKey="assertDiscount"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontRemoveCartItemActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontRemoveCartItemActionGroup.xml new file mode 100644 index 0000000000000..f2d4088370a2b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontRemoveCartItemActionGroup.xml @@ -0,0 +1,13 @@ +<?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="StorefrontRemoveCartItemActionGroup"> + <click selector="{{CheckoutCartProductSection.RemoveItem}}" stepKey="deleteProductFromCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml index a77b07a129dce..cf7f2baeb4b26 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml @@ -9,6 +9,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="CheckoutCartPage" url="/checkout/cart" module="Magento_Checkout" area="storefront"> + <section name="StorefrontCartToolbarSection"/> <section name="CheckoutCartProductSection"/> <section name="CheckoutCartSummarySection"/> <section name="CheckoutCartCrossSellSection"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml index d3fa045e4654f..d6173dfa17916 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml @@ -14,5 +14,6 @@ <section name="CheckoutOrderSummarySection"/> <section name="CheckoutSuccessMainSection"/> <section name="CheckoutPaymentSection"/> + <section name="SelectShippingBillingPopupSection"/> </page> </pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index 3ab3fa5857b78..af9d81249e8ac 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -18,6 +18,7 @@ <element name="ProductRegularPriceByName" type="text" selector="//div[descendant::*[contains(text(), '{{var1}}')]]//*[contains(@class, 'subtotal')]" parameterized="true"/> + <element name="productFirstPrice" type="text" selector="td[class~=price] span[class='price']"/> <element name="ProductImageByName" type="text" selector="//main//table[@id='shopping-cart-table']//tbody//tr//img[contains(@class, 'product-image-photo') and @alt='{{var1}}']" parameterized="true"/> @@ -32,6 +33,7 @@ selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/> <element name="removeProductByName" type="text" selector="//*[contains(text(), '{{productName}}')]/ancestor::tbody//a[@class='action action-delete']" parameterized="true" timeout="30"/> <element name="productName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']"/> + <element name="moveToWishlistByProductName" type="button" selector="//a[contains(text(), '{{productName}}')]/ancestor::tbody/tr//a[contains(@class, 'towishlist')]" parameterized="true"/> <element name="nthItemOption" type="block" selector=".item:nth-of-type({{numElement}}) .item-options" parameterized="true"/> <element name="nthEditButton" type="block" selector=".item:nth-of-type({{numElement}}) .action-edit" parameterized="true"/> <element name="nthBundleOptionName" type="text" selector=".product-item-details .item-options:nth-of-type({{numOption}}) dt" parameterized="true"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index 477451ef003ce..de71fc3f8ad0e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -9,6 +9,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutCartSummarySection"> + <element name="orderTotal" type="input" selector=".grand.totals .amount .price"/> + <element name="subTotal" type="input" selector="span[data-th='Subtotal']"/> <element name="expandShoppingCartSummary" type="button" selector="//*[contains(@class, 'items-in-cart')][not(contains(@class, 'active'))]"/> <element name="elementPosition" type="text" selector=".data.table.totals > tbody tr:nth-of-type({{value}}) > th" parameterized="true"/> <element name="subtotal" type="text" selector="//*[@id='cart-totals']//tr[@class='totals sub']//td//span[@class='price']"/> @@ -20,7 +22,7 @@ <element name="totalAmount" type="text" selector="//*[@id='cart-totals']//tr[@class='grand totals']//td//span[@class='price' and contains(text(), '{{amount}}')]" parameterized="true"/> <element name="proceedToCheckout" type="button" selector=".action.primary.checkout span" timeout="30"/> <element name="discountAmount" type="text" selector="td[data-th='Discount']"/> - <element name="shippingHeading" type="button" selector="#block-shipping-heading" timeout="60"/> + <element name="shippingHeading" type="button" selector="#block-shipping-heading" timeout="10"/> <element name="postcode" type="input" selector="input[name='postcode']" timeout="10"/> <element name="stateProvince" type="select" selector="select[name='region_id']" timeout="10"/> <element name="stateProvinceInput" type="input" selector="input[name='region']"/> @@ -33,5 +35,6 @@ <element name="methodName" type="text" selector="#co-shipping-method-form label"/> <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> + <element name="estimateShippingAndTaxForm" type="block" selector="#shipping-zip-form"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml index d3ad2aed96946..026265656379a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml @@ -19,5 +19,7 @@ <element name="additionalAddress" type="text" selector=".block.block-addresses-list"/> <element name="miniCartTabClosed" type="button" selector=".title[aria-expanded='false']" timeout="30"/> <element name="itemsQtyInCart" type="text" selector=".items-in-cart > .title > strong > span"/> + <element name="orderSummaryShippingTotalLabelDescription" type="text" selector=".shipping.totals .label.description"/> + <element name="shippingTotalNotYetCalculated" type="text" selector=".shipping.totals .not-calculated"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 903c21d7ec0ca..16fd373d3ae4d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -31,7 +31,8 @@ <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active" timeout="30"/> <element name="checkMoneyOrderPayment" type="radio" selector="input#checkmo.radio" timeout="30"/> <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> - <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[text()='Payment Method']" /> + <element name="placeOrderWithoutTimeout" type="button" selector=".payment-method._active button.action.primary.checkout"/> + <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[@data-role='title']" /> <element name="orderSummarySubtotal" type="text" selector="//tr[@class='totals sub']//span[@class='price']" /> <element name="orderSummaryShippingTotal" type="text" selector="//tr[@class='totals shipping excl']//span[@class='price']" /> <element name="orderSummaryShippingMethod" type="text" selector="//tr[@class='totals shipping excl']//span[@class='value']" /> @@ -42,6 +43,7 @@ <element name="ProductOptionLinkActiveByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options active']//a[text() = '{{var2}}']" parameterized="true" /> <element name="shipToInformation" type="text" selector="//div[@class='ship-to']//div[@class='shipping-information-content']" /> <element name="shippingMethodInformation" type="text" selector="//div[@class='ship-via']//div[@class='shipping-information-content']" /> + <element name="shippingInformationSection" type="text" selector=".ship-to .shipping-information-content" /> <element name="paymentMethodTitle" type="text" selector=".payment-method-title span" /> <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> @@ -51,6 +53,7 @@ <element name="orderSummaryTotalIncluding" type="text" selector="//tr[@class='grand totals incl']//span[@class='price']" /> <element name="orderSummaryTotalExcluding" type="text" selector="//tr[@class='grand totals excl']//span[@class='price']" /> <element name="shippingAndBillingAddressSame" type="input" selector="#billing-address-same-as-shipping-braintree_cc_vault"/> + <element name="myShippingAndBillingAddressSame" type="input" selector=".billing-address-same-as-shipping-block"/> <element name="addressAction" type="button" selector="//span[text()='{{action}}']" parameterized="true"/> <element name="addressBook" type="button" selector="//a[text()='Address Book']"/> <element name="noQuotes" type="text" selector=".no-quotes-block"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml index 08a9d671a8d02..c486e13ecf58b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml @@ -14,6 +14,7 @@ <element name="orderNumber" type="text" selector="div.checkout-success > p:nth-child(1) > span"/> <element name="orderNumber22" type="text" selector=".order-number>strong"/> <element name="orderLink" type="text" selector="a[href*=order_id].order-number" timeout="30"/> + <element name="orderLinks" type="text" selector="a[href*=order_id]" timeout="30"/> <element name="orderNumberText" type="text" selector=".checkout-success > p:nth-child(1)"/> <element name="continueShoppingButton" type="button" selector=".action.primary.continue" timeout="30"/> <element name="createAnAccount" type="button" selector="[data-bind*="i18n: 'Create an Account'"]" timeout="30"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCartToolbarSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCartToolbarSection.xml new file mode 100644 index 0000000000000..ff40449369530 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCartToolbarSection.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="StorefrontCartToolbarSection"> + <element name="toolbarNumber" type="text" selector="div.toolbar > .pager > .toolbar-amount > .toolbar-number" /> + <element name="toolbarPager" type="text" selector="div.toolbar > .pager > .pages" /> + <element name="toolbarNextPage" type="text" selector="div.toolbar > .pager > .pages > .pages-item-next" /> + <element name="toolbarPreviousPage" type="text" selector="div.toolbar > .pager > .pages > .pages-item-previous" /> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index 3e1de2b14ba62..80ed4f90c2cd0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -13,7 +13,9 @@ <element name="productCount" type="text" selector="//header//div[contains(@class, 'minicart-wrapper')]//a[contains(@class, 'showcart')]//span[@class='counter-number']"/> <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> - <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> + <element name="productPriceByItsName" type="text" selector="//a[normalize-space()='{{prodName}}']/../following-sibling::*//*[@class='price']" parameterized="true"/> + <element name="productImageByName" type="text" selector="header ol[id='mini-cart'] span[class='product-image-container'] img[alt='{{prodName}}']" parameterized="true"/> + <element name="productImageByItsName" type="text" selector="img[alt='{{prodName}}']" parameterized="true"/> <element name="productName" type="text" selector=".product-item-name"/> <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> @@ -23,6 +25,7 @@ <element name="goToCheckout" type="button" selector="#top-cart-btn-checkout" timeout="30"/> <element name="viewAndEditCart" type="button" selector=".action.viewcart" timeout="30"/> <element name="miniCartItemsText" type="text" selector=".minicart-items"/> + <element name="editMiniCartItem" type="button" selector=".action.edit" timeout="30"/> <element name="deleteMiniCartItem" type="button" selector=".action.delete" timeout="30"/> <element name="deleteMiniCartItemByName" type="button" selector="//ol[@id='mini-cart']//div[contains(., '{{var}}')]//a[contains(@class, 'delete')]" parameterized="true"/> <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml index 9714b76a05613..163e71c50053f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml @@ -30,7 +30,7 @@ <!--Logout from customer account--> <amOnPage url="customer/account/logout/" stepKey="logoutCustomerOne"/> <waitForPageLoad stepKey="waitLogoutCustomerOne"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> </after> @@ -147,7 +147,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml index 1a51e1e02fe86..e16ef70c23e3d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create downloadable product --> <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink"> @@ -30,6 +31,7 @@ </createData> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete downloadable product --> <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml index 20015f76e08e3..c61545e51d535 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -25,6 +25,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + <!--Clear cache and reindex--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 5335ec2ad775d..4281a0eb77da8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -184,6 +184,194 @@ <argument name="productVar" value="$$createSimpleProduct2$$"/> </actionGroup> + <comment userInput="Place order with check money order payment" stepKey="commentPlaceOrderWithCheckMoneyOrderPayment" after="guestCheckoutCheckSimpleProduct2InCartItems" /> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" after="commentPlaceOrderWithCheckMoneyOrderPayment"/> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="guestSeeBillingAddress" after="guestSelectCheckMoneyOrderPayment"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceorder" after="guestSeeBillingAddress"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <comment userInput="End of checking out" stepKey="endOfCheckingOut" after="guestPlaceorder" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <!-- Step 3: User adds products to cart --> + <comment userInput="Start of adding products to cart" stepKey="startOfAddingProductsToCart" after="endOfBrowsingCatalog"/> + <!-- Add Simple Product 1 to cart --> + <comment userInput="Add Simple Product 1 to cart" stepKey="commentAddSimpleProduct1ToCart" after="startOfAddingProductsToCart" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory" after="commentAddSimpleProduct1ToCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartCategoryloaded" after="cartClickCategory"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="cartAssertCategory" after="waitForCartCategoryloaded"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="cartAssertSimpleProduct1" after="cartAssertCategory"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="cartGrabSimpleProduct1ImageSrc" after="cartAssertSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartGrabSimpleProduct1ImageSrc" stepKey="cartAssertSimpleProduct1ImageNotDefault" after="cartGrabSimpleProduct1ImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="cartClickSimpleProduct1" after="cartAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartSimpleProduct1loaded" after="cartClickSimpleProduct1"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertProduct1Page" after="waitForCartSimpleProduct1loaded"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartGrabSimpleProduct1PageImageSrc" after="cartAssertProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartGrabSimpleProduct1PageImageSrc" stepKey="cartAssertSimpleProduct1PageImageNotDefault" after="cartGrabSimpleProduct1PageImageSrc"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddProduct1ToCart" after="cartAssertSimpleProduct1PageImageNotDefault"> + <argument name="product" value="$$createSimpleProduct1$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Add Simple Product 2 to cart --> + <comment userInput="Add Simple Product 2 to cart" stepKey="commentAddSimpleProduct2ToCart" after="cartAddProduct1ToCart" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory1" after="commentAddSimpleProduct2ToCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartCategory1loaded" after="cartClickCategory1"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="cartAssertCategory1ForSimpleProduct2" after="waitForCartCategory1loaded"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="cartAssertSimpleProduct2" after="cartAssertCategory1ForSimpleProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="cartGrabSimpleProduct2ImageSrc" after="cartAssertSimpleProduct2"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartGrabSimpleProduct2ImageSrc" stepKey="cartAssertSimpleProduct2ImageNotDefault" after="cartGrabSimpleProduct2ImageSrc"/> + <actionGroup ref="StorefrontAddCategoryProductToCartActionGroup" stepKey="cartAddProduct2ToCart" after="cartAssertSimpleProduct2ImageNotDefault"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productCount" value="CONST.two"/> + </actionGroup> + + <!-- Check products in minicart --> + <!-- Check simple product 1 in minicart --> + <comment userInput="Check simple product 1 in minicart" stepKey="commentCheckSimpleProduct1InMinicart" after="cartAddProduct2ToCart"/> + <actionGroup ref="StorefrontOpenMinicartAndCheckSimpleProductActionGroup" stepKey="cartOpenMinicartAndCheckSimpleProduct1" after="commentCheckSimpleProduct1InMinicart"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct1ImageSrc" after="cartOpenMinicartAndCheckSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/thumbnail\.jpg/'" actual="$cartMinicartGrabSimpleProduct1ImageSrc" stepKey="cartMinicartAssertSimpleProduct1ImageNotDefault" after="cartMinicartGrabSimpleProduct1ImageSrc"/> + <click selector="{{StorefrontMinicartSection.productLinkByName($$createSimpleProduct1.name$$)}}" stepKey="cartMinicartClickSimpleProduct1" after="cartMinicartAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForMinicartSimpleProduct1loaded" after="cartMinicartClickSimpleProduct1"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertMinicartProduct1Page" after="waitForMinicartSimpleProduct1loaded"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct1PageImageSrc" after="cartAssertMinicartProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartMinicartGrabSimpleProduct1PageImageSrc" stepKey="cartMinicartAssertSimpleProduct1PageImageNotDefault" after="cartMinicartGrabSimpleProduct1PageImageSrc"/> + <actionGroup ref="StorefrontOpenMinicartAndCheckSimpleProductActionGroup" stepKey="cartOpenMinicartAndCheckSimpleProduct2" after="cartMinicartAssertSimpleProduct1PageImageNotDefault"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- Check simple product2 in minicart --> + <comment userInput="Check simple product 2 in minicart" stepKey="commentCheckSimpleProduct2InMinicart" after="cartOpenMinicartAndCheckSimpleProduct2"/> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct2ImageSrc" after="commentCheckSimpleProduct2InMinicart"/> + <assertNotRegExp expected="'/placeholder\/thumbnail\.jpg/'" actual="$cartMinicartGrabSimpleProduct2ImageSrc" stepKey="cartMinicartAssertSimpleProduct2ImageNotDefault" after="cartMinicartGrabSimpleProduct2ImageSrc"/> + <click selector="{{StorefrontMinicartSection.productLinkByName($$createSimpleProduct2.name$$)}}" stepKey="cartMinicartClickSimpleProduct2" after="cartMinicartAssertSimpleProduct2ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForMinicartSimpleProduct2loaded" after="cartMinicartClickSimpleProduct2"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertMinicartProduct2Page" after="waitForMinicartSimpleProduct2loaded"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct2PageImageSrc" after="cartAssertMinicartProduct2Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartMinicartGrabSimpleProduct2PageImageSrc" stepKey="cartMinicartAssertSimpleProduct2PageImageNotDefault" after="cartMinicartGrabSimpleProduct2PageImageSrc"/> + + <!-- Check products in cart --> + <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> + </actionGroup> + + <!-- Check simple product 1 in cart --> + <comment userInput="Check simple product 1 in cart" stepKey="commentCheckSimpleProduct1InCart" after="cartAssertCart"/> + <actionGroup ref="StorefrontCheckCartSimpleProductActionGroup" stepKey="cartAssertCartSimpleProduct1" after="commentCheckSimpleProduct1InCart"> + <argument name="product" value="$$createSimpleProduct1$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{CheckoutCartProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="cartCartGrabSimpleProduct1ImageSrc" after="cartAssertCartSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartCartGrabSimpleProduct1ImageSrc" stepKey="cartCartAssertSimpleProduct1ImageNotDefault" after="cartCartGrabSimpleProduct1ImageSrc"/> + <click selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProduct1.name$$)}}" stepKey="cartClickCartSimpleProduct1" after="cartCartAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartSimpleProduct1loadedAgain" after="cartClickCartSimpleProduct1"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertCartProduct1Page" after="waitForCartSimpleProduct1loadedAgain"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartCartGrabSimpleProduct2PageImageSrc1" after="cartAssertCartProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartCartGrabSimpleProduct2PageImageSrc1" stepKey="cartCartAssertSimpleProduct2PageImageNotDefault1" after="cartCartGrabSimpleProduct2PageImageSrc1"/> + + <!-- Check simple product 2 in cart --> + <comment userInput="Check simple product 2 in cart" stepKey="commentCheckSimpleProduct2InCart" after="cartCartAssertSimpleProduct2PageImageNotDefault1"/> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart1" after="commentCheckSimpleProduct2InCart"/> + <actionGroup ref="StorefrontCheckCartSimpleProductActionGroup" stepKey="cartAssertCartSimpleProduct2" after="cartOpenCart1"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{CheckoutCartProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="cartCartGrabSimpleProduct2ImageSrc" after="cartAssertCartSimpleProduct2"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartCartGrabSimpleProduct2ImageSrc" stepKey="cartCartAssertSimpleProduct2ImageNotDefault" after="cartCartGrabSimpleProduct2ImageSrc"/> + <click selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProduct2.name$$)}}" stepKey="cartClickCartSimpleProduct2" after="cartCartAssertSimpleProduct2ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartSimpleProduct2loaded" after="cartClickCartSimpleProduct2"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertCartProduct2Page" after="waitForCartSimpleProduct2loaded"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartCartGrabSimpleProduct2PageImageSrc2" after="cartAssertCartProduct2Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartCartGrabSimpleProduct2PageImageSrc2" stepKey="cartCartAssertSimpleProduct2PageImageNotDefault2" after="cartCartGrabSimpleProduct2PageImageSrc2"/> + <comment userInput="End of adding products to cart" stepKey="endOfAddingProductsToCart" after="cartCartAssertSimpleProduct2PageImageNotDefault2" /> + + <!-- Step 6: Check out --> + <comment userInput="Start of checking out" stepKey="startOfCheckingOut" after="endOfUsingCouponCode" /> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" after="startOfCheckingOut"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection" after="guestGoToCheckoutFromMinicart"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + + <!-- Check order summary in checkout --> + <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="guestCheckoutFillingShippingSection" /> + <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="guestCheckoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> + <argument name="subtotal" value="480.00"/> + <argument name="shippingTotal" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> + </actionGroup> + + <!-- Check ship to information in checkout --> + <comment userInput="Check ship to information in checkout" stepKey="commentCheckShipToInformationInCheckout" after="guestCheckoutCheckOrderSummary" /> + <actionGroup ref="CheckShipToInformationInCheckoutActionGroup" stepKey="guestCheckoutCheckShipToInformation" after="commentCheckShipToInformationInCheckout"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + + <!-- Check shipping method in checkout --> + <comment userInput="Check shipping method in checkout" stepKey="commentCheckShippingMethodInCheckout" after="guestCheckoutCheckShipToInformation" /> + <actionGroup ref="CheckShippingMethodInCheckoutActionGroup" stepKey="guestCheckoutCheckShippingMethod" after="commentCheckShippingMethodInCheckout"> + <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod" /> + </actionGroup> + + <!-- Verify Simple Product 1 is in checkout cart items --> + <comment userInput="Verify Simple Product 1 is in checkout cart items" stepKey="commentVerifySimpleProduct1IsInCheckoutCartItems" after="guestCheckoutCheckShippingMethod" /> + <actionGroup ref="CheckProductInCheckoutCartItemsActionGroup" stepKey="guestCheckoutCheckSimpleProduct1InCartItems" after="commentVerifySimpleProduct1IsInCheckoutCartItems"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + + <!-- Verify Simple Product 2 is in checkout cart items --> + <comment userInput="Verify Simple Product 2 is in checkout cart items" stepKey="commentVerifySimpleProduct2IsInCheckoutCartItems" after="guestCheckoutCheckSimpleProduct1InCartItems" /> + <actionGroup ref="CheckProductInCheckoutCartItemsActionGroup" stepKey="guestCheckoutCheckSimpleProduct2InCartItems" after="commentVerifySimpleProduct2IsInCheckoutCartItems"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + <comment userInput="Place order with check money order payment" stepKey="commentPlaceOrderWithCheckMoneyOrderPayment" after="guestCheckoutCheckSimpleProduct2InCartItems" /> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" after="commentPlaceOrderWithCheckMoneyOrderPayment"/> <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="guestSeeBillingAddress" after="guestSelectCheckMoneyOrderPayment"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml index 3c98f9177f4a7..fd6656b1d1b28 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml @@ -26,6 +26,8 @@ <field key="price">100.00</field> <requiredEntity createDataKey="createCategory"/> </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml index 3ec73aec580d5..e85a47ab7a91d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -90,6 +91,7 @@ <magentoCLI command="indexer:reindex" stepKey="reindex"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml index 9c00f2be1d60b..d67800e21afc2 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml @@ -109,7 +109,7 @@ <argument name="total" value="110.00"/> </actionGroup> - <!--Enabled Shopping Cart Sidebar --> + <!--Enabled Mini Cart --> <magentoCLI stepKey="enableShoppingCartSidebar" command="config:set checkout/sidebar/display 1"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> <reloadPage stepKey="reloadThePage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml index 09608eef7178a..e3090d6cb311b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml @@ -20,6 +20,9 @@ <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRatePrice"/> + <magentoCLI command="config:set {{EnableFlatRateToAllAllowedCountriesConfigData.path}} {{EnableFlatRateToAllAllowedCountriesConfigData.value}}" stepKey="allowFlatRateToAllCountries"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <!-- Create Default Category --> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml index 5a46dbc90e207..ec9852a6a939d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml @@ -18,6 +18,7 @@ </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> @@ -31,6 +32,7 @@ <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <deleteData createDataKey="createDownloadableProduct" stepKey="deleteProduct"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml index 09a5ce4c70373..8b8aed3ac6204 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml @@ -15,9 +15,6 @@ <testCaseId value="MC-14720"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> @@ -280,7 +277,9 @@ </actionGroup> <!-- Verify Product11 is not displayed in Mini Cart --> - <dontSee selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="$$simpleProduct11.name$$" stepKey="dontSeeProduct11NameInMiniCart"/> - <dontSee selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="110" stepKey="dontSeeProduct11PriceInMiniCart"/> + <actionGroup ref="AssertStorefrontMiniCartProductDetailsAbsentActionGroup" stepKey="assertSimpleProduct11IsNotInMiniCart"> + <argument name="productName" value="$$simpleProduct11.name$$"/> + <argument name="productPrice" value="$110.00"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml new file mode 100644 index 0000000000000..9dbd5daba6f23 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml @@ -0,0 +1,143 @@ +<?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="StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check if the cart pager is visible with more than 20 cart items and the pager disappears if an item is removed from cart"/> + <title value="Test if the cart pager is visible with more than 20 cart items and the pager disappears if an item is removed from cart."/> + <description value="Test if the cart pager is visible with more than 20 cart items and the pager disappears if an item is removed from cart."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14700"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Set the default number of items on cart which is 20--> + <magentoCLI stepKey="allowSpecificValue" command="config:set checkout/cart/number_items_to_display_pager 20" /> + + <createData entity="SimpleTwo" stepKey="simpleProduct1"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem1"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct2"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem2"> + <argument name="product" value="$simpleProduct2$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct3"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem3"> + <argument name="product" value="$simpleProduct3$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct4"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem4"> + <argument name="product" value="$simpleProduct4$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct5"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem5"> + <argument name="product" value="$simpleProduct5$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct6"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem6"> + <argument name="product" value="$simpleProduct6$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct7"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem7"> + <argument name="product" value="$simpleProduct7$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct8"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem8"> + <argument name="product" value="$simpleProduct8$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct9"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem9"> + <argument name="product" value="$simpleProduct9$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct10"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem10"> + <argument name="product" value="$$simpleProduct10$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct11"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem11"> + <argument name="product" value="$$simpleProduct11$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct12"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem12"> + <argument name="product" value="$$simpleProduct12$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct13"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem13"> + <argument name="product" value="$$simpleProduct13$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct14"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem14"> + <argument name="product" value="$$simpleProduct14$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct15"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem15"> + <argument name="product" value="$$simpleProduct15$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct16"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem16"> + <argument name="product" value="$$simpleProduct16$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct17"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem17"> + <argument name="product" value="$$simpleProduct17$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct18"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem18"> + <argument name="product" value="$$simpleProduct18$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct19"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem19"> + <argument name="product" value="$$simpleProduct19$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct20"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem20"> + <argument name="product" value="$$simpleProduct20$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct21"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem21"> + <argument name="product" value="$$simpleProduct21$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteCartItem1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteCartItem2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteCartItem3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteCartItem4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteCartItem5"/> + <deleteData createDataKey="simpleProduct6" stepKey="deleteCartItem6"/> + <deleteData createDataKey="simpleProduct7" stepKey="deleteCartItem7"/> + <deleteData createDataKey="simpleProduct8" stepKey="deleteCartItem8"/> + <deleteData createDataKey="simpleProduct9" stepKey="deleteCartItem9"/> + <deleteData createDataKey="simpleProduct10" stepKey="deleteCartItem10"/> + <deleteData createDataKey="simpleProduct11" stepKey="deleteCartItem11"/> + <deleteData createDataKey="simpleProduct12" stepKey="deleteCartItem12"/> + <deleteData createDataKey="simpleProduct13" stepKey="deleteCartItem13"/> + <deleteData createDataKey="simpleProduct14" stepKey="deleteCartItem14"/> + <deleteData createDataKey="simpleProduct15" stepKey="deleteCartItem15"/> + <deleteData createDataKey="simpleProduct16" stepKey="deleteCartItem16"/> + <deleteData createDataKey="simpleProduct17" stepKey="deleteCartItem17"/> + <deleteData createDataKey="simpleProduct18" stepKey="deleteCartItem18"/> + <deleteData createDataKey="simpleProduct19" stepKey="deleteCartItem19"/> + <deleteData createDataKey="simpleProduct20" stepKey="deleteCartItem20"/> + <deleteData createDataKey="simpleProduct21" stepKey="deleteCartItem21"/> + </after> + <actionGroup ref="StorefrontOpenCartPageActionGroup" stepKey="goToCartPage" /> + <actionGroup ref="AssertToolbarTextIsVisibleInCartActionGroup" stepKey="VerifyPagerText"> + <argument name="text" value="Items 1 to 20 of 21 total"/> + </actionGroup> + <actionGroup ref="StorefrontRemoveCartItemActionGroup" stepKey="removeCartItem" /> + <actionGroup ref="AssertPagerTextIsNotVisibleActionGroup" stepKey="VerifyMissingPagerText2" > + <argument name="text" value="Items 1 to 20"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml new file mode 100644 index 0000000000000..97eceae962bfb --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml @@ -0,0 +1,80 @@ +<?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="StorefrontCheckoutDisabledBundleProductTest"> + <annotations> + <features value="Checkout"/> + <stories value="Disabled bundle product is preventing customer to checkout for the first attempt"/> + <title value="Customer should be able to checkout if there is at least one available product in the cart"/> + <description value="Customer should be able to checkout if there is at least one available product in the cart"/> + <severity value="MINOR"/> + <testCaseId value="MC-29105"/> + <group value="checkout"/> + </annotations> + + <before> + <!-- Create category and simple product --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create bundle product --> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleDynamicProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleDynamicProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createNewBundleLink"> + <requiredEntity createDataKey="createBundleDynamicProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="cacheFlush"/> + </before> + <after> + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete bundle product data --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createBundleDynamicProduct" stepKey="deleteBundleProduct"/> + </after> + <!-- Add simple product to the cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Go to bundle product page --> + <amOnPage url="{{StorefrontProductPage.url($$createBundleDynamicProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!-- Add bundle product to the cart --> + <click selector="{{StorefrontBundleProductActionSection.customizeAndAddToCartButton}}" stepKey="clickCustomizeAndAddToCart"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createBundleDynamicProduct.name$$"/> + </actionGroup> + <!-- Login to admin panel --> + <openNewTab stepKey="openNewTab"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Find the first simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <!-- Disabled bundle product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> + <argument name="product" value="$$createBundleDynamicProduct$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab"/> + <!-- Go to cart page--> + <actionGroup ref="StorefrontOpenCartPageActionGroup" stepKey="openCartPage"/> + <!-- Assert checkout button exists on the page--> + <seeElement selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="seeCheckoutButton"/> + <!-- Assert no error message is not shown on the page--> + <dontSee userInput="Some of the products are out of stock." stepKey="seeNoItemsInShoppingCart"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml new file mode 100644 index 0000000000000..0e704e5336db9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml @@ -0,0 +1,91 @@ +<?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="StorefrontCustomerCheckoutDisabledProductAndCouponTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Customer can login if product in his cart was disabled"/> + <description value="Customer can login with disabled product in the cart and a coupon applied"/> + <severity value="MINOR"/> + <testCaseId value="MC-21996"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createUSCustomer"/> + <!-- Create sales rule with coupon --> + <createData entity="SalesRuleSpecificCouponAndByPercent" stepKey="createSalesRule"/> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createSalesRule"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createUSCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> + </after> + + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createUSCustomer$$" /> + </actionGroup> + + <!-- Add product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Open View and edit --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="clickMiniCart1"/> + + <!-- Fill the Estimate Shipping and Tax section --> + <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"/> + + <!-- Apply Coupon --> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyDiscount"> + <argument name="coupon" value="$$createCouponForCartPriceRule$$"/> + </actionGroup> + + <!-- Sign out Customer from storefront --> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="customerLogout"/> + + <!-- Login to admin panel --> + <openNewTab stepKey="openNewTab"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Find the first simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + + <!-- Disabled simple product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab"/> + + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLoginSecondTime"> + <argument name="Customer" value="$$createUSCustomer$$" /> + </actionGroup> + + <!-- Check cart --> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart2"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml index 40b781df9b2ae..218ff959750d6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml @@ -125,6 +125,10 @@ </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + + <!--TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> </before> <after> <!-- Go to the tax rule page and delete the row we created--> @@ -148,7 +152,7 @@ <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> <deleteData createDataKey="multiple_address_customer" stepKey="deleteCustomer"/> @@ -221,6 +225,10 @@ <magentoCLI command="config:set payment/checkmo/allowspecific 1" stepKey="allowSpecificValue" /> <magentoCLI command="config:set payment/checkmo/specificcountry GB" stepKey="specificCountryValue" /> <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -270,4 +278,4 @@ <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> </actionGroup> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml index 6a3f6ab4f7058..0fa503e1783b5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml @@ -19,6 +19,7 @@ </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> @@ -30,6 +31,7 @@ <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <deleteData createDataKey="createDownloadableProduct" stepKey="deleteProduct"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml index a77341b8697b5..a0914cfc27138 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -23,9 +23,13 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> @@ -77,11 +81,8 @@ <title value="Guest Checkout when Cart sidebar disabled"/> <description value="Should be able to place an order as a Guest when Cart sidebar is disabled"/> <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-97001"/> + <testCaseId value="MC-16587"/> <group value="checkout"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> <magentoCLI stepKey="disableSidebar" command="config:set checkout/sidebar/display 0" /> @@ -109,10 +110,11 @@ </createData> <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 1" /> <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry GB" /> - + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 0" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml new file mode 100644 index 0000000000000..afe4ebcfea40c --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml @@ -0,0 +1,134 @@ +<?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="StorefrontMissingPagerShoppingCartWith20ProductsTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check if the cart pager is missing with 20 cart items"/> + <title value="Test if the cart pager is missing with 20 cart items."/> + <description value="Test if the cart pager is missing with 20 cart items."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14698"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Set the default number of items on cart which is 20--> + <magentoCLI stepKey="allowSpecificValue" command="config:set checkout/cart/number_items_to_display_pager 20" /> + <createData entity="SimpleTwo" stepKey="simpleProduct1"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem1"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct2"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct3"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem3"> + <argument name="product" value="$$simpleProduct3$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct4"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem4"> + <argument name="product" value="$$simpleProduct4$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct5"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem5"> + <argument name="product" value="$$simpleProduct5$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct6"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem6"> + <argument name="product" value="$$simpleProduct6$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct7"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem7"> + <argument name="product" value="$$simpleProduct7$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct8"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem8"> + <argument name="product" value="$$simpleProduct8$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct9"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem9"> + <argument name="product" value="$$simpleProduct9$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct10"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem10"> + <argument name="product" value="$$simpleProduct10$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct11"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem11"> + <argument name="product" value="$$simpleProduct11$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct12"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem12"> + <argument name="product" value="$$simpleProduct12$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct13"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem13"> + <argument name="product" value="$$simpleProduct13$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct14"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem14"> + <argument name="product" value="$$simpleProduct14$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct15"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem15"> + <argument name="product" value="$$simpleProduct15$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct16"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem16"> + <argument name="product" value="$$simpleProduct16$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct17"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem17"> + <argument name="product" value="$$simpleProduct17$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct18"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem18"> + <argument name="product" value="$$simpleProduct18$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct19"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem19"> + <argument name="product" value="$$simpleProduct19$$"/> + </actionGroup> + <createData entity="SimpleTwo" stepKey="simpleProduct20"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="CartItem20"> + <argument name="product" value="$$simpleProduct20$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteCartItem1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteCartItem2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteCartItem3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteCartItem4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteCartItem5"/> + <deleteData createDataKey="simpleProduct6" stepKey="deleteCartItem6"/> + <deleteData createDataKey="simpleProduct7" stepKey="deleteCartItem7"/> + <deleteData createDataKey="simpleProduct8" stepKey="deleteCartItem8"/> + <deleteData createDataKey="simpleProduct9" stepKey="deleteCartItem9"/> + <deleteData createDataKey="simpleProduct10" stepKey="deleteCartItem10"/> + <deleteData createDataKey="simpleProduct11" stepKey="deleteCartItem11"/> + <deleteData createDataKey="simpleProduct12" stepKey="deleteCartItem12"/> + <deleteData createDataKey="simpleProduct13" stepKey="deleteCartItem13"/> + <deleteData createDataKey="simpleProduct14" stepKey="deleteCartItem14"/> + <deleteData createDataKey="simpleProduct15" stepKey="deleteCartItem15"/> + <deleteData createDataKey="simpleProduct16" stepKey="deleteCartItem16"/> + <deleteData createDataKey="simpleProduct17" stepKey="deleteCartItem17"/> + <deleteData createDataKey="simpleProduct18" stepKey="deleteCartItem18"/> + <deleteData createDataKey="simpleProduct19" stepKey="deleteCartItem19"/> + <deleteData createDataKey="simpleProduct20" stepKey="deleteCartItem20"/> + </after> + <!-- Go to the shopping cart and check if the pager is missing--> + <actionGroup ref="StorefrontOpenCartPageActionGroup" stepKey="goToCartPage" /> + <actionGroup ref="AssertPagerTextIsNotVisibleActionGroup" stepKey="VerifyMissingPagerText" > + <argument name="text" value="Items 1 to 20"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml new file mode 100644 index 0000000000000..8ec89ce1fe8f0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml @@ -0,0 +1,209 @@ +<?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="StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest"> + <annotations> + <title value="Not applicable Shipping Method In Review and Payment Step"/> + <stories value="Checkout Shipping Method Recalculation after Coupon Code Added"/> + <description value="User should not be able to place order when free shipping declined after applying coupon code"/> + <features value="Checkout"/> + <severity value="MAJOR"/> + <testCaseId value="MC-22625"/> + <useCaseId value="MC-21926"/> + <group value="Checkout"/> + <skip> + <issueId value="MC-29597"/> + </skip> + </annotations> + + <before> + <!-- Enable Free Shipping Method and set Minimum Order Amount to 100--> + <magentoCLI command="config:set {{AdminFreeshippingActiveConfigData.path}} {{AdminFreeshippingActiveConfigData.enabled}}" stepKey="enableFreeShippingMethod" /> + <magentoCLI command="config:set {{AdminFreeshippingMinimumOrderAmountConfigData.path}} {{AdminFreeshippingMinimumOrderAmountConfigData.hundred}}" stepKey="setFreeShippingMethodMinimumOrderAmountToBe100" /> + + <!--Set Fedex configs data--> + <magentoCLI command="config:set {{AdminFedexEnableForCheckoutConfigData.path}} {{AdminFedexEnableForCheckoutConfigData.value}}" stepKey="enableCheckout"/> + <magentoCLI command="config:set {{AdminFedexEnableSandboxModeConfigData.path}} {{AdminFedexEnableSandboxModeConfigData.value}}" stepKey="enableSandbox"/> + <magentoCLI command="config:set {{AdminFedexEnableDebugConfigData.path}} {{AdminFedexEnableDebugConfigData.value}}" stepKey="enableDebug"/> + <magentoCLI command="config:set {{AdminFedexEnableShowMethodConfigData.path}} {{AdminFedexEnableShowMethodConfigData.value}}" stepKey="enableShowMethod"/> + + <!--Set StoreInformation configs data--> + <magentoCLI command="config:set {{AdminGeneralSetStoreNameConfigData.path}} '{{AdminGeneralSetStoreNameConfigData.value}}'" stepKey="setStoreInformationName"/> + <magentoCLI command="config:set {{AdminGeneralSetStorePhoneConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.telephone}}" stepKey="setStoreInformationPhone"/> + <magentoCLI command="config:set {{AdminGeneralSetCountryConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="setStoreInformationCountry"/> + <magentoCLI command="config:set {{AdminGeneralSetCityConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="setStoreInformationCity"/> + <magentoCLI command="config:set {{AdminGeneralSetPostcodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setStoreInformationPostcode"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setStoreInformationStreetAddress"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setStoreInformationStreetAddress2"/> + <magentoCLI command="config:set {{AdminGeneralSetVatNumberConfigData.path}} {{AdminGeneralSetVatNumberConfigData.value}}" stepKey="setStoreInformationVatNumber"/> + + <!--Set Shipping settings origin data--> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCountryConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="setOriginCountry"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCityConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="setOriginCity"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setOriginZipCode"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setOriginStreetAddress"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setOriginStreetAddress2"/> + + <!-- Create Simple Product --> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"> + <field key="price">100</field> + </createData> + <!-- Create Cart Price Rule with 10% discount --> + <createData entity="ApiSalesRule" stepKey="createCartPriceRule"/> + <!-- Create Coupon code for the Cart Price Rule --> + <createData entity="ApiSalesRuleCoupon" stepKey="createCartPriceRuleCoupon"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + <!-- Create Customer with filled Shipping & Billing Address --> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <magentoCLI command="config:set {{AdminFreeshippingMinimumOrderAmountConfigData.path}} {{AdminFreeshippingMinimumOrderAmountConfigData.default}}" stepKey="setFreeShippingMethodMinimumOrderAmountAsDefault" /> + <magentoCLI command="config:set {{AdminFreeshippingActiveConfigData.path}} {{AdminFreeshippingActiveConfigData.disabled}}" stepKey="disableFreeShippingMethod" /> + <!--Reset configs--> + <magentoCLI command="config:set {{AdminFedexDisableForCheckoutConfigData.path}} {{AdminFedexDisableForCheckoutConfigData.value}}" stepKey="disableCheckout"/> + <magentoCLI command="config:set {{AdminFedexDisableSandboxModeConfigData.path}} {{AdminFedexDisableSandboxModeConfigData.value}}" stepKey="disableSandbox"/> + <magentoCLI command="config:set {{AdminFedexDisableDebugConfigData.path}} {{AdminFedexDisableDebugConfigData.value}}" stepKey="disableDebug"/> + <magentoCLI command="config:set {{AdminFedexDisableShowMethodConfigData.path}} {{AdminFedexDisableShowMethodConfigData.value}}" stepKey="disableShowMethod"/> + <magentoCLI command="config:set {{AdminGeneralSetStoreNameConfigData.path}} ''" stepKey="setStoreInformationName"/> + <magentoCLI command="config:set {{AdminGeneralSetStorePhoneConfigData.path}} ''" stepKey="setStoreInformationPhone"/> + <magentoCLI command="config:set {{AdminGeneralSetCityConfigData.path}} ''" stepKey="setStoreInformationCity"/> + <magentoCLI command="config:set {{AdminGeneralSetPostcodeConfigData.path}} ''" stepKey="setStoreInformationPostcode"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddressConfigData.path}} ''" stepKey="setStoreInformationStreetAddress"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddress2ConfigData.path}} ''" stepKey="setStoreInformationStreetAddress2"/> + <magentoCLI command="config:set {{AdminGeneralSetVatNumberConfigData.path}} ''" stepKey="setStoreInformationVatNumber"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCityConfigData.path}} ''" stepKey="setOriginCity"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} ''" stepKey="setOriginZipCode"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} ''" stepKey="setOriginStreetAddress"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} ''" stepKey="setOriginStreetAddress2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + + <!-- Guest Customer Test Scenario --> + <!-- Add Simple Product to Cart --> + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout"/> + + <!-- Fill all required fields --> + <actionGroup ref="GuestCheckoutFillNewShippingAddressActionGroup" stepKey="fillNewShippingAddress"> + <argument name="customer" value="Simple_Customer_Without_Address" /> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select Free Shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodFreeShipping"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStep" stepKey="goToCheckoutReview"/> + + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <!-- Select payment solution --> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + + <!-- Apply Discount Coupon to the Order --> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> + <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> + </actionGroup> + + <!-- Assert Shipping total is not yet calculated --> + <actionGroup ref="AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup" stepKey="assertNotYetCalculated"/> + + <!-- Assert order cannot be placed and error message will shown. --> + <actionGroup ref="AssertStorefrontOrderCannotBePlacedActionGroup" stepKey="assertOrderCannotBePlaced"> + <argument name="error" value="The shipping method is missing. Select the shipping method and try again."/> + </actionGroup> + + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage"/> + + <!-- Chose flat rate --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodFlatRate"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStep" stepKey="goToCheckoutReview2"/> + + <!-- Place order assert succeed --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> + + <!-- Loged in Customer Test Scenario --> + <!-- Login with created Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add Simple Product to Cart --> + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" stepKey="addProductToCart2"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout2"/> + + <!-- Select Free Shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodFreeShipping2"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStep" stepKey="goToCheckoutReview3"/> + + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> + + <!-- Select payment solution --> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution2" /> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> + + <!-- Apply Discount Coupon to the Order --> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon2"> + <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> + </actionGroup> + + <!-- Assert Shipping total is not yet calculated --> + <actionGroup ref="AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup" stepKey="assertNotYetCalculated2"/> + + <!-- Assert order cannot be placed and error message will shown. --> + <actionGroup ref="AssertStorefrontOrderCannotBePlacedActionGroup" stepKey="assertOrderCannotBePlaced2"> + <argument name="error" value="The shipping method is missing. Select the shipping method and try again."/> + </actionGroup> + + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage2"/> + + <!-- Chose flat rate --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodFlatRate2"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStep" stepKey="goToCheckoutReview4"/> + + <!-- Place order assert succeed --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder2"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml index 63751ad697ede..8ed8e590eb229 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml @@ -11,6 +11,7 @@ <test name="StorefrontOnePageCheckoutJsValidationTest"> <annotations> <features value="Checkout"/> + <stories value="Checkout"/> <title value="Js validation error messages must be absent for required fields after checkout start."/> <description value="Js validation error messages must be absent for required fields after checkout start."/> <severity value="MAJOR" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml new file mode 100644 index 0000000000000..44bfe81b40dc0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml @@ -0,0 +1,44 @@ +<?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="StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest"> + <annotations> + <features value="Checkout"/> + <stories value="Region updates after changing country "/> + <title value="Region updates after changing country "/> + <description value="Region dupdates after changing country and leaving region select unselected"/> + <severity value="CRITICAL"/> + <testCaseId value="https://github.com/magento/magento2/issues/23460"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Login to storefront from customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBookPage"> + <argument name="menu" value="Address Book"/> + </actionGroup> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickEditAddress"/> + <selectOption selector="{{StorefrontCustomerAddressFormSection.country}}" userInput="{{updateCustomerFranceAddress.country}}" stepKey="selectCountry"/> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{updateCustomerFranceAddress.country}}" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest.xml new file mode 100644 index 0000000000000..744401cf24d13 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest.xml @@ -0,0 +1,45 @@ +<?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="StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check if the cart pager is visible with 2 cart items and one item per page"/> + <title value="Test if the cart pager is visible with 2 cart items and one item per page."/> + <description value="Test if the cart pager is visible with 2 cart items and one item per page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14701"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Changing the number of items to display in cart--> + <magentoCLI stepKey="allowSpecificValue" command="config:set checkout/cart/number_items_to_display_pager 1" /> + <createData entity="SimpleTwo" stepKey="createSimpleProduct1"/> + <createData entity="SimpleTwo" stepKey="createSimpleProduct2"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + </before> + <after> + <!--Set back the default number of items on cart which is 20--> + <magentoCLI stepKey="allowSpecificValue" command="config:set checkout/cart/number_items_to_display_pager 20" /> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> + </after> + <actionGroup ref="StorefrontOpenCartPageActionGroup" stepKey="goToCartPage" /> + <actionGroup ref="AssertToolbarTextIsVisibleInCartActionGroup" stepKey="VerifyPagerTextWithChangedConfiguration"> + <argument name="text" value="Items 1 to 1 of 2 total"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml index ebf24e710fe39..482e2fb6233a6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml @@ -18,7 +18,7 @@ </annotations> <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <magentoCLI command="downloadable:domains:add" arguments="example.com static.magento.com" stepKey="addDownloadableDomain"/> <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"> <field key="price">20.00</field> </createData> @@ -38,10 +38,12 @@ </createData> </before> <after> + <magentoCLI command="downloadable:domains:remove" arguments="example.com static.magento.com" stepKey="removeDownloadableDomain"/> <deleteData createDataKey="createDownloadableProduct" stepKey="deleteProduct"/> <deleteData createDataKey="virtualProduct" stepKey="deleteVirtualProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> <actionGroup ref="logout" stepKey="logout"/> </after> @@ -106,6 +108,9 @@ <seeElement selector="{{StorefrontMiniCartSection.emptyMiniCart}}" stepKey="assertEmptyCart" /> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> + <!-- Login to Admin Page --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!-- Open Order Index Page --> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> <waitForPageLoad stepKey="waitForPageLoad5"/> @@ -127,4 +132,4 @@ <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$15.00" stepKey="seeGrandTotal"/> <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderStatus"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/DirectoryDataTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/DirectoryDataTest.php new file mode 100644 index 0000000000000..3c0bae31c9c0d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/DirectoryDataTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\CustomerData; + +use Magento\Checkout\CustomerData\DirectoryData; +use Magento\Directory\Helper\Data as HelperData; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Directory\Model\Country; +use PHPUnit\Framework\TestCase; + +class DirectoryDataTest extends TestCase +{ + /** + * @var DirectoryData + */ + private $model; + + /** + * @var HelperData|\PHPUnit_Framework_MockObject_MockObject + */ + private $directoryHelperMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManager; + + /** + * Setup environment for testing + */ + protected function setUp() + { + $this->objectManager = new ObjectManagerHelper($this); + $this->directoryHelperMock = $this->createMock(HelperData::class); + + $this->model = $this->objectManager->getObject( + DirectoryData::class, + [ + 'directoryHelper' => $this->directoryHelperMock + ] + ); + } + + /** + * Test getSectionData() function + */ + public function testGetSectionData() + { + $regions = [ + 'US' => [ + 'TX' => [ + 'code' => 'TX', + 'name' => 'Texas' + ] + ] + ]; + + $testCountryInfo = $this->objectManager->getObject(Country::class); + $testCountryInfo->setData('country_id', 'US'); + $testCountryInfo->setData('iso2_code', 'US'); + $testCountryInfo->setData('iso3_code', 'USA'); + $testCountryInfo->setData('name_default', 'United States of America'); + $testCountryInfo->setData('name_en_US', 'United States of America'); + $countries = ['US' => $testCountryInfo]; + + $this->directoryHelperMock->expects($this->any()) + ->method('getRegionData') + ->willReturn($regions); + + $this->directoryHelperMock->expects($this->any()) + ->method('getCountryCollection') + ->willReturn($countries); + + /* Assert result */ + $this->assertEquals( + [ + 'US' => [ + 'name' => 'United States of America', + 'regions' => [ + 'TX' => [ + 'code' => 'TX', + 'name' => 'Texas' + ] + ] + ] + ], + $this->model->getSectionData() + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php index daabb080b1c9a..82384fa83ab94 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php @@ -48,6 +48,9 @@ public function testProcess($cartData, $expected) $this->assertEquals($this->requestProcessor->process($cartData), $expected); } + /** + * @return array + */ public function cartDataProvider() { return [ diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php index 3cc80e14fd026..350f9954208fa 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php @@ -43,7 +43,7 @@ protected function setUp() ); $this->checkoutSessionMock = $this->createPartialMock(\Magento\Checkout\Model\Session::class, ['clearStorage']); $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); + $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); $this->cacheConfigMock = $this->createMock(\Magento\PageCache\Model\Config::class); $this->depersonalizeCheckerMock = $this->createMock(\Magento\PageCache\Model\DepersonalizeChecker::class); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php b/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php index 26234992e6136..969631901adff 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php @@ -9,7 +9,8 @@ */ namespace Magento\Checkout\Test\Unit\Model; -use \Magento\Checkout\Model\Session; +use Magento\Checkout\Model\Session; +use Magento\Framework\Exception\NoSuchEntityException; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -374,6 +375,68 @@ public function testGetStepData() $this->assertEquals($stepData['complex']['key'], $session->getStepData('complex', 'key')); } + /** + * Ensure that if quote not exist for customer quote will be null + * + * @return void + */ + public function testGetQuote(): void + { + $storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); + $customerSession = $this->createMock(\Magento\Customer\Model\Session::class); + $quoteRepository = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); + $quoteFactory = $this->createMock(\Magento\Quote\Model\QuoteFactory::class); + $quote = $this->createMock(\Magento\Quote\Model\Quote::class); + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $loggerMethods = get_class_methods(\Psr\Log\LoggerInterface::class); + + $quoteFactory->expects($this->once()) + ->method('create') + ->willReturn($quote); + $customerSession->expects($this->exactly(3)) + ->method('isLoggedIn') + ->willReturn(true); + $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->setMethods(['getWebsiteId', '__wakeup']) + ->getMock(); + $storeManager->expects($this->any()) + ->method('getStore') + ->will($this->returnValue($store)); + $storage = $this->getMockBuilder(\Magento\Framework\Session\Storage::class) + ->disableOriginalConstructor() + ->setMethods(['setData', 'getData']) + ->getMock(); + $storage->expects($this->at(0)) + ->method('getData') + ->willReturn(1); + $quoteRepository->expects($this->once()) + ->method('getActiveForCustomer') + ->willThrowException(new NoSuchEntityException()); + + foreach ($loggerMethods as $method) { + $logger->expects($this->never())->method($method); + } + + $quote->expects($this->once()) + ->method('setCustomer') + ->with(null); + + $constructArguments = $this->_helper->getConstructArguments( + \Magento\Checkout\Model\Session::class, + [ + 'storeManager' => $storeManager, + 'quoteRepository' => $quoteRepository, + 'customerSession' => $customerSession, + 'storage' => $storage, + 'quoteFactory' => $quoteFactory, + 'logger' => $logger + ] + ); + $this->_session = $this->_helper->getObject(\Magento\Checkout\Model\Session::class, $constructArguments); + $this->_session->getQuote(); + } + public function testSetStepData() { $stepData = [ diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 2afa796443e75..399474a36bfc7 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -57,9 +57,9 @@ </field> </group> <group id="sidebar" translate="label" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Shopping Cart Sidebar</label> + <label>Mini Cart</label> <field id="display" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Display Shopping Cart Sidebar</label> + <label>Display Mini Cart</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="count" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 90c2878f501cf..021cd930c74c0 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> <action name="checkout/cart/add"> <section name="cart"/> + <section name="directory-data"/> </action> <action name="checkout/cart/delete"> <section name="cart"/> @@ -41,7 +42,6 @@ </action> <action name="rest/*/V1/carts/*/payment-information"> <section name="cart"/> - <section name="checkout-data"/> <section name="last-ordered-items"/> </action> <action name="rest/*/V1/guest-carts/*/payment-information"> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 7f2f0b4390321..251985faf6cc4 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -156,8 +156,8 @@ Shipping,Shipping "Number of Items to Display Pager","Number of Items to Display Pager" "My Cart Link","My Cart Link" "Display Cart Summary","Display Cart Summary" -"Shopping Cart Sidebar","Shopping Cart Sidebar" -"Display Shopping Cart Sidebar","Display Shopping Cart Sidebar" +"Mini Cart","Mini Cart" +"Display Mini Cart","Display Mini Cart" "Number of Items to Display Scrollbar","Number of Items to Display Scrollbar" "Maximum Number of Items to Display","Maximum Number of Items to Display" "Payment Failed Emails","Payment Failed Emails" diff --git a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html index 03ad7d9e8d848..6d2b27fd0e293 100644 --- a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html +++ b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html @@ -6,13 +6,13 @@ --> <!--@subject {{trans "Payment Transaction Failed Reminder"}} @--> <!--@vars { -"var billingAddress.format('html')|raw":"Billing Address", +"var billingAddressHtml|raw":"Billing Address", "var checkoutType":"Checkout Type", "var customerEmail":"Customer Email", "var customer":"Customer Name", "var dateAndTime":"Date and Time of Transaction", "var paymentMethod":"Payment Method", -"var shippingAddress.format('html')|raw":"Shipping Address", +"var shippingAddressHtml|raw":"Shipping Address", "var shippingMethod":"Shipping Method", "var items|raw":"Shopping Cart Items", "var total":"Total", @@ -44,11 +44,11 @@ <h1>{{trans "Payment Transaction Failed"}}</h1> </li> <li> <strong>{{trans "Billing Address:"}}</strong><br /> - {{var billingAddress.format('html')|raw}} + {{var billingAddressHtml|raw}} </li> <li> <strong>{{trans "Shipping Address:"}}</strong><br /> - {{var shippingAddress.format('html')|raw}} + {{var shippingAddressHtml|raw}} </li> <li> <strong>{{trans "Shipping Method:"}}</strong><br /> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index a305413bcf1f3..fed0c951eff9f 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -118,7 +118,7 @@ </item> <item name="component" xsi:type="string">Magento_Checkout/js/view/shipping</item> <item name="provider" xsi:type="string">checkoutProvider</item> - <item name="sortOrder" xsi:type="string">1</item> + <item name="sortOrder" xsi:type="string">10</item> <item name="children" xsi:type="array"> <item name="customer-email" xsi:type="array"> <item name="component" xsi:type="string">Magento_Checkout/js/view/form/element/email</item> @@ -246,6 +246,7 @@ <item name="component" xsi:type="string">Magento_Checkout/js/view/payment</item> <item name="config" xsi:type="array"> <item name="title" xsi:type="string" translate="true">Payment</item> + <item name="sortOrder" xsi:type="string">20</item> </item> <item name="children" xsi:type="array"> <item name="renders" xsi:type="array"> diff --git a/app/code/Magento/Checkout/view/frontend/templates/button.phtml b/app/code/Magento/Checkout/view/frontend/templates/button.phtml index b0087794ea850..6d1f076e6b26d 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/button.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/button.phtml @@ -7,7 +7,10 @@ <?php /** @var $block \Magento\Checkout\Block\Onepage\Success */ ?> <?php if ($block->getCanViewOrder() && $block->getCanPrintOrder()) :?> - <a href="<?= $block->escapeUrl($block->getPrintUrl()) ?>" target="_blank" class="print"> + <a href="<?= $block->escapeUrl($block->getPrintUrl()) ?>" + class="action print" + target="_blank" + rel="noopener"> <?= $block->escapeHtml(__('Print receipt')) ?> </a> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml index bf8490affea0c..65dc514e476ff 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml @@ -7,10 +7,13 @@ /** * @var \Magento\Framework\View\Element\AbstractBlock $block */ + +// We should use strlen function because coupon code could be "0", converted to bool will lead to false +$hasCouponCode = (bool) strlen($block->getCouponCode()); ?> <div class="block discount" id="block-discount" - data-mage-init='{"collapsible":{"openedState": "active", "saveState": false}}' + data-mage-init='{"collapsible":{"active": <?= $hasCouponCode ? 'true' : 'false' ?>, "openedState": "active", "saveState": false}}' > <div class="title" data-role="title"> <strong id="block-discount-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Apply Discount Code')) ?></strong> @@ -23,7 +26,7 @@ "removeCouponSelector": "#remove-coupon", "applyButton": "button.action.apply", "cancelButton": "button.action.cancel"}}'> - <div class="fieldset coupon<?= strlen($block->getCouponCode()) ? ' applied' : '' ?>"> + <div class="fieldset coupon<?= $hasCouponCode ? ' applied' : '' ?>"> <input type="hidden" name="remove" id="remove-coupon" value="0" /> <div class="field"> <label for="coupon_code" class="label"><span><?= $block->escapeHtml(__('Enter discount code')) ?></span></label> @@ -34,14 +37,14 @@ name="coupon_code" value="<?= $block->escapeHtmlAttr($block->getCouponCode()) ?>" placeholder="<?= $block->escapeHtmlAttr(__('Enter discount code')) ?>" - <?php if (strlen($block->getCouponCode())) :?> + <?php if ($hasCouponCode) :?> disabled="disabled" <?php endif; ?> /> </div> </div> <div class="actions-toolbar"> - <?php if (!strlen($block->getCouponCode())) :?> + <?php if (!$hasCouponCode) :?> <div class="primary"> <button class="action apply primary" type="button" value="<?= $block->escapeHtmlAttr(__('Apply Discount')) ?>"> <span><?= $block->escapeHtml(__('Apply Discount')) ?></span> @@ -54,7 +57,7 @@ <?php endif; ?> </div> </div> - <?php if (!strlen($block->getCouponCode())) : ?> + <?php if (!$hasCouponCode) : ?> <?= /* @noEscape */ $block->getChildHtml('captcha') ?> <?php endif; ?> </form> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index e1ab036c7d889..370d70c44d886 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -56,7 +56,7 @@ <span><?= $block->escapeHtml(__('Continue Shopping')) ?></span> </a> <?php endif; ?> - <button type="submit" + <button type="button" name="update_cart_action" data-cart-empty="" value="empty_cart" diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index 86c9a357af23c..0267f68c5f81d 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -104,6 +104,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima value="<?= $block->escapeHtmlAttr($block->getQty()) ?>" type="number" size="4" + step="any" title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" class="input-text qty" data-validate="{required:true,'validate-greater-than-zero':true}" diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml index 2a7ccc38e9d83..b3b94cfae18dc 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml @@ -28,9 +28,9 @@ $taxDataHelper = $this->helper(Magento\Tax\Helper\Data::class); <dt><?= $block->escapeHtml($_option['label']) ?></dt> <dd> <?php if (isset($_formatedOptionValue['full_view'])) :?> - <?= $block->escapeHtml($_formatedOptionValue['full_view']) ?> + <?= /* @noEscape */ $_formatedOptionValue['full_view'] ?> <?php else :?> - <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?= /* @noEscape */ $_formatedOptionValue['value'] ?> <?php endif; ?> </dd> <?php endforeach; ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/recollect-shipping-rates.js b/app/code/Magento/Checkout/view/frontend/web/js/action/recollect-shipping-rates.js new file mode 100644 index 0000000000000..7cce025c4eafc --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/recollect-shipping-rates.js @@ -0,0 +1,26 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/action/select-shipping-address', + 'Magento_Checkout/js/model/shipping-rate-registry' +], function (quote, selectShippingAddress, rateRegistry) { + 'use strict'; + + return function () { + var shippingAddress = null; + + if (!quote.isVirtual()) { + shippingAddress = quote.shippingAddress(); + + rateRegistry.set(shippingAddress.getCacheKey(), null); + selectShippingAddress(shippingAddress); + } + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js b/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js index 702df47526715..34f1700749794 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js @@ -12,6 +12,11 @@ define([ 'use strict'; return function (paymentMethod) { + if (paymentMethod) { + paymentMethod.__disableTmpl = { + title: true + }; + } quote.paymentMethod(paymentMethod); }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js index 4085da82f4151..9de8a93905c99 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js @@ -13,14 +13,33 @@ define([ 'Magento_Checkout/js/model/error-processor', 'Magento_Customer/js/model/customer', 'Magento_Checkout/js/action/get-totals', - 'Magento_Checkout/js/model/full-screen-loader' -], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { + 'Magento_Checkout/js/model/full-screen-loader', + 'underscore' +], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader, _) { 'use strict'; + /** + * Filter template data. + * + * @param {Object|Array} data + */ + var filterTemplateData = function (data) { + return _.each(data, function (value, key, list) { + if (_.isArray(value) || _.isObject(value)) { + list[key] = filterTemplateData(value); + } + + if (key === '__disableTmpl') { + delete list[key]; + } + }); + }; + return function (messageContainer, paymentData, skipBilling) { var serviceUrl, payload; + paymentData = filterTemplateData(paymentData); skipBilling = skipBilling || false; payload = { cartId: quote.getQuoteId(), diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index 9b20a782c38d9..6e1b031ab48ce 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -9,8 +9,9 @@ define([ 'jquery', 'Magento_Checkout/js/model/new-customer-address', 'Magento_Customer/js/customer-data', - 'mage/utils/objects' -], function ($, address, customerData, mageUtils) { + 'mage/utils/objects', + 'underscore' +], function ($, address, customerData, mageUtils, _) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -18,6 +19,7 @@ define([ return { /** * Convert address form data to Address object + * * @param {Object} formData * @returns {Object} */ @@ -59,13 +61,15 @@ define([ delete addressData['region_id']; if (addressData['custom_attributes']) { - addressData['custom_attributes'] = Object.entries(addressData['custom_attributes']) - .map(function (customAttribute) { + addressData['custom_attributes'] = _.map( + addressData['custom_attributes'], + function (value, key) { return { - 'attribute_code': customAttribute[0], - 'value': customAttribute[1] + 'attribute_code': key, + 'value': value }; - }); + } + ); } return address(addressData); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index 54e496131972e..fd12eed76ed50 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -13,8 +13,8 @@ define([ ], function (quote, defaultProcessor, totalsDefaultProvider, shippingService, cartCache, customerData) { 'use strict'; - var rateProcessors = [], - totalsProcessors = [], + var rateProcessors = {}, + totalsProcessors = {}, /** * Estimate totals for shipping address and update shipping rates. diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index bc0ab59b622a2..66539ad211859 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -148,7 +148,7 @@ define([ var selectedShippingRate = checkoutData.getSelectedShippingRate(), availableRate = false; - if (ratesData.length === 1) { + if (ratesData.length === 1 && !quote.shippingMethod()) { //set shipping rate if we have only one available shipping rate selectShippingMethodAction(ratesData[0]); @@ -169,10 +169,12 @@ define([ } if (!availableRate && window.checkoutConfig.selectedShippingMethod) { - availableRate = window.checkoutConfig.selectedShippingMethod; - selectShippingMethodAction(window.checkoutConfig.selectedShippingMethod); + availableRate = _.find(ratesData, function (rate) { + var selectedShippingMethod = window.checkoutConfig.selectedShippingMethod; - return; + return rate['carrier_code'] == selectedShippingMethod['carrier_code'] && //eslint-disable-line + rate['method_code'] == selectedShippingMethod['method_code']; //eslint-disable-line eqeqeq + }); } //Unset selected shipping method if not available diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js index 7eddc0d1a58d4..be2199961e07a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js @@ -10,7 +10,7 @@ define([ ], function (quote, defaultProcessor, customerAddressProcessor) { 'use strict'; - var processors = []; + var processors = {}; processors.default = defaultProcessor; processors['customer-address'] = customerAddressProcessor; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js index d506f0a4359c5..cf26f682ad3aa 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js @@ -11,7 +11,7 @@ define([ ], function (defaultProcessor) { 'use strict'; - var processors = []; + var processors = {}; processors['default'] = defaultProcessor; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index a9cbb1194cfd3..6d54f607484b4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -162,6 +162,9 @@ define([ this._clearError(); this._checkRegionRequired(country); + $(regionList).find('option:selected').removeAttr('selected'); + regionInput.val(''); + // Populate state/province dropdown list if available or use input box if (this.options.regionJson[country]) { this._removeSelectOptions(regionList); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index 39bd07f0c73a0..b15599673095f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -14,14 +14,14 @@ define([ _create: function () { var items, i, reload; - $(this.options.emptyCartButton).on('click', $.proxy(function (event) { - if (event.detail === 0) { - return; - } - + $(this.options.emptyCartButton).on('click', $.proxy(function () { $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); $(this.options.updateCartActionContainer) .attr('name', 'update_cart_action').attr('value', 'empty_cart'); + + if ($(this.options.emptyCartButton).parents('form').length > 0) { + $(this.options.emptyCartButton).parents('form').submit(); + } }, this)); items = $.find('[data-role="cart-item-qty"]'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index f58a560a6b3ca..6fc5ef9d2a574 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -13,7 +13,8 @@ define([ 'jquery-ui-modules/widget', 'mage/decorate', 'mage/collapsible', - 'mage/cookies' + 'mage/cookies', + 'jquery-ui-modules/effect-fade' ], function ($, authenticationPopup, customerData, alert, confirm, _) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 59d1daa757138..e728a5c0fcdd5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -159,7 +159,6 @@ function ( } addressData['save_in_address_book'] = this.saveInAddressBook() ? 1 : 0; newBillingAddress = createBillingAddress(addressData); - // New address must be selected as a billing address selectBillingAddress(newBillingAddress); checkoutData.setSelectedBillingAddress(newBillingAddress.getKey()); @@ -237,6 +236,30 @@ function ( */ getCode: function (parent) { return _.isFunction(parent.getCode) ? parent.getCode() : 'shared'; + }, + + /** + * Get customer attribute label + * + * @param {*} attribute + * @returns {*} + */ + getCustomAttributeLabel: function (attribute) { + var resultAttribute; + + if (typeof attribute === 'string') { + return attribute; + } + + if (attribute.label) { + return attribute.label; + } + + resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { + value: attribute.value + }); + + return resultAttribute && resultAttribute.label || attribute.value; } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js index c0de643d3a223..c570bda51a80e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js @@ -145,12 +145,19 @@ define([ var loginFormSelector = 'form[data-role=email-with-possible-login]', usernameSelector = loginFormSelector + ' input[name=username]', loginForm = $(loginFormSelector), - validator; + validator, + valid; loginForm.validation(); if (focused === false && !!this.email()) { - return !!$(usernameSelector).valid(); + valid = !!$(usernameSelector).valid(); + + if (valid) { + $(usernameSelector).removeAttr('aria-invalid aria-describedby'); + } + + return valid; } validator = loginForm.validate(); @@ -185,6 +192,10 @@ define([ * @returns {Boolean} - initial visibility state. */ resolveInitialPasswordVisibility: function () { + if (checkoutData.getInputFieldEmailValue() !== '' && checkoutData.getCheckedEmailValue() === '') { + return true; + } + if (checkoutData.getInputFieldEmailValue() !== '') { return checkoutData.getInputFieldEmailValue() === checkoutData.getCheckedEmailValue(); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index d152f94397730..4adc1cd88c0ae 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -103,8 +103,8 @@ define([ }); if ( - cartData().website_id !== window.checkout.websiteId && - cartData().website_id !== undefined + cartData().website_id !== window.checkout.websiteId && cartData().website_id !== undefined || + cartData().storeId !== window.checkout.storeId && cartData().storeId !== undefined ) { customerData.reload(['cart'], false); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js index e8994c61b7221..716fb367b2e1f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js @@ -54,7 +54,7 @@ define([ $t('Review & Payments'), this.isVisible, _.bind(this.navigate, this), - 20 + this.sortOrder ); return this; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js index 54381ad96b0b9..939a2af1a25aa 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js @@ -7,12 +7,13 @@ define([ 'jquery', 'ko', 'uiComponent', + 'underscore', 'Magento_Checkout/js/action/select-shipping-address', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/shipping-address/form-popup-state', 'Magento_Checkout/js/checkout-data', 'Magento_Customer/js/customer-data' -], function ($, ko, Component, selectShippingAddressAction, quote, formPopUpState, checkoutData, customerData) { +], function ($, ko, Component, _, selectShippingAddressAction, quote, formPopUpState, checkoutData, customerData) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -47,6 +48,30 @@ define([ return countryData()[countryId] != undefined ? countryData()[countryId].name : ''; //eslint-disable-line }, + /** + * Get customer attribute label + * + * @param {*} attribute + * @returns {*} + */ + getCustomAttributeLabel: function (attribute) { + var resultAttribute; + + if (typeof attribute === 'string') { + return attribute; + } + + if (attribute.label) { + return attribute.label; + } + + resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { + value: attribute.value + }); + + return resultAttribute && resultAttribute.label || attribute.value; + }, + /** Set selected customer shipping address */ selectAddress: function () { selectShippingAddressAction(this.address()); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js index 4f4fc3de3e1a5..2bdfd063cb6fb 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js @@ -16,7 +16,8 @@ define([ var defaultRendererTemplate = { parent: '${ $.$data.parentName }', name: '${ $.$data.name }', - component: 'Magento_Checkout/js/view/shipping-address/address-renderer/default' + component: 'Magento_Checkout/js/view/shipping-address/address-renderer/default', + provider: 'checkoutProvider' }; return Component.extend({ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js index 2158873842687..73c9c53147c70 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information.js @@ -31,13 +31,17 @@ define([ var shippingMethod = quote.shippingMethod(), shippingMethodTitle = ''; + if (!shippingMethod) { + return ''; + } + + shippingMethodTitle = shippingMethod['carrier_title']; + if (typeof shippingMethod['method_title'] !== 'undefined') { - shippingMethodTitle = ' - ' + shippingMethod['method_title']; + shippingMethodTitle += ' - ' + shippingMethod['method_title']; } - return shippingMethod ? - shippingMethod['carrier_title'] + shippingMethodTitle : - shippingMethod['carrier_title']; + return shippingMethodTitle; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js index acc9f1c2391d9..009178cbb19b9 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js @@ -5,8 +5,9 @@ define([ 'uiComponent', + 'underscore', 'Magento_Customer/js/customer-data' -], function (Component, customerData) { +], function (Component, _, customerData) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -22,6 +23,30 @@ define([ */ getCountryName: function (countryId) { return countryData()[countryId] != undefined ? countryData()[countryId].name : ''; //eslint-disable-line + }, + + /** + * Get customer attribute label + * + * @param {*} attribute + * @returns {*} + */ + getCustomAttributeLabel: function (attribute) { + var resultAttribute; + + if (typeof attribute === 'string') { + return attribute; + } + + if (attribute.label) { + return attribute.label; + } + + resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { + value: attribute.value + }); + + return resultAttribute && resultAttribute.label || attribute.value; } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js index 28eb83c8be3e3..3bb2715c78a7b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js @@ -16,7 +16,8 @@ define([ var defaultRendererTemplate = { parent: '${ $.$data.parentName }', name: '${ $.$data.name }', - component: 'Magento_Checkout/js/view/shipping-information/address-renderer/default' + component: 'Magento_Checkout/js/view/shipping-information/address-renderer/default', + provider: 'checkoutProvider' }; return Component.extend({ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index c811d3a1e8369..fe8d7782e5eae 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -60,7 +60,10 @@ define([ template: 'Magento_Checkout/shipping', shippingFormTemplate: 'Magento_Checkout/shipping-address/form', shippingMethodListTemplate: 'Magento_Checkout/shipping-address/shipping-method-list', - shippingMethodItemTemplate: 'Magento_Checkout/shipping-address/shipping-method-item' + shippingMethodItemTemplate: 'Magento_Checkout/shipping-address/shipping-method-item', + imports: { + countryOptions: '${ $.parentName }.shippingAddress.shipping-address-fieldset.country_id:indexedOptions' + } }, visible: ko.observable(!quote.isVirtual()), errorValidationMessage: ko.observable(false), @@ -87,7 +90,7 @@ define([ '', $t('Shipping'), this.visible, _.bind(this.navigate, this), - 10 + this.sortOrder ); } checkoutDataResolver.resolveShippingAddress(); @@ -249,6 +252,16 @@ define([ if (this.validateShippingInformation()) { quote.billingAddress(null); checkoutDataResolver.resolveBillingAddress(); + registry.async('checkoutProvider')(function (checkoutProvider) { + var shippingAddressData = checkoutData.getShippingAddressFromData(); + + if (shippingAddressData) { + checkoutProvider.set( + 'shippingAddress', + $.extend(true, {}, checkoutProvider.get('shippingAddress'), shippingAddressData) + ); + } + }); setShippingInformationAction().done( function () { stepNavigator.next(); @@ -266,9 +279,7 @@ define([ loginFormSelector = 'form[data-role=email-with-possible-login]', emailValidationResult = customer.isLoggedIn(), field, - country = registry.get(this.parentName + '.shippingAddress.shipping-address-fieldset.country_id'), - countryIndexedOptions = country.indexedOptions, - option = countryIndexedOptions[quote.shippingAddress().countryId], + option = _.isObject(this.countryOptions) && this.countryOptions[quote.shippingAddress().countryId], messageContainer = registry.get('checkout.errors').messageContainer; if (!quote.shippingMethod()) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js index 10d49265e3bb9..a0bbc9dd55bff 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js @@ -5,9 +5,11 @@ define([ 'jquery', + 'underscore', 'Magento_Checkout/js/view/summary/abstract-total', - 'Magento_Checkout/js/model/quote' -], function ($, Component, quote) { + 'Magento_Checkout/js/model/quote', + 'Magento_SalesRule/js/view/summary/discount' +], function ($, _, Component, quote, discountView) { 'use strict'; return Component.extend({ @@ -21,7 +23,7 @@ define([ * @return {*} */ getShippingMethodTitle: function () { - var shippingMethod = '', + var shippingMethod, shippingMethodTitle = ''; if (!this.isCalculated()) { @@ -29,11 +31,15 @@ define([ } shippingMethod = quote.shippingMethod(); + if (!_.isArray(shippingMethod) && !_.isObject(shippingMethod)) { + return ''; + } + if (typeof shippingMethod['method_title'] !== 'undefined') { shippingMethodTitle = ' - ' + shippingMethod['method_title']; } - return shippingMethod ? + return shippingMethodTitle ? shippingMethod['carrier_title'] + shippingMethodTitle : shippingMethod['carrier_title']; }, @@ -57,6 +63,34 @@ define([ price = this.totals()['shipping_amount']; return this.getFormattedPrice(price); + }, + + /** + * If is set coupon code, but there wasn't displayed discount view. + * + * @return {Boolean} + */ + haveToShowCoupon: function () { + var couponCode = this.totals()['coupon_code']; + + if (typeof couponCode === 'undefined') { + couponCode = false; + } + + return couponCode && !discountView().isDisplayed(); + }, + + /** + * Returns coupon code description. + * + * @return {String} + */ + getCouponDescription: function () { + if (!this.haveToShowCoupon()) { + return ''; + } + + return '(' + this.totals()['coupon_code'] + ')'; } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index a0827d17d6622..23bbce48fee2c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -13,19 +13,8 @@ <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> <each args="data: currentBillingAddress().customAttributes, as: 'element'"> - <if args="typeof element === 'object'"> - <if args="element.label"> - <text args="element.label"/> - </if> - <ifnot args="element.label"> - <if args="element.value"> - <text args="element.value"/> - </if> - </ifnot> - </if> - <if args="typeof element === 'string'"> - <text args="element"/> - </if><br/> + <text args="$parent.getCustomAttributeLabel(element)"/> + <br/> </each> <button visible="!isAddressSameAsShipping()" diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html index cf64c0140b955..b14f4da3f5f7d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html @@ -13,21 +13,8 @@ <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> <each args="data: address().customAttributes, as: 'element'"> - <each args="data: Object.keys(element), as: 'attribute'"> - <if args="typeof element[attribute] === 'object'"> - <if args="element[attribute].label"> - <text args="element[attribute].label"/> - </if> - <ifnot args="element[attribute].label"> - <if args="element[attribute].value"> - <text args="element[attribute].value"/> - </if> - </ifnot> - </if> - <if args="typeof element[attribute] === 'string'"> - <text args="element[attribute]"/> - </if><br/> - </each> + <text args="$parent.getCustomAttributeLabel(element)"/> + <br/> </each> <button visible="address().isEditable()" type="button" diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html index 75e061426d816..26dd7742d1da8 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html @@ -13,18 +13,7 @@ <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> <each args="data: address().customAttributes, as: 'element'"> - <if args="typeof element === 'object'"> - <if args="element.label"> - <text args="element.label"/> - </if> - <ifnot args="element.label"> - <if args="element.value"> - <text args="element.value"/> - </if> - </ifnot> - </if> - <if args="typeof element === 'string'"> - <text args="element"/> - </if><br/> + <text args="$parent.getCustomAttributeLabel(element)"/> + <br/> </each> </if> diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 1217270d780e1..ff77db60a64e6 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -102,7 +102,8 @@ protected function getAgreementsConfig() : nl2br($this->escaper->escapeHtml($agreement->getContent())), 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), - 'agreementId' => $agreement->getAgreementId() + 'agreementId' => $agreement->getAgreementId(), + 'contentHeight' => $agreement->getContentHeight() ]; } diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index c8309bacb0a86..6b8477e0b4919 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -77,6 +77,7 @@ public function testGetConfigIfContentIsHtml() $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; + $contentHeight = '100px'; $expectedResult = [ 'checkoutAgreements' => [ 'isEnabled' => 1, @@ -86,6 +87,7 @@ public function testGetConfigIfContentIsHtml() 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, 'agreementId' => $agreementId, + 'contentHeight' => $contentHeight ], ], ], @@ -116,6 +118,7 @@ public function testGetConfigIfContentIsHtml() $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); $agreement->expects($this->once())->method('getMode')->willReturn($mode); $agreement->expects($this->once())->method('getAgreementId')->willReturn($agreementId); + $agreement->expects($this->once())->method('getContentHeight')->willReturn($contentHeight); $this->assertEquals($expectedResult, $this->model->getConfig()); } @@ -133,6 +136,7 @@ public function testGetConfigIfContentIsNotHtml() $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; + $contentHeight = '100px'; $expectedResult = [ 'checkoutAgreements' => [ 'isEnabled' => 1, @@ -142,6 +146,7 @@ public function testGetConfigIfContentIsNotHtml() 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, 'agreementId' => $agreementId, + 'contentHeight' => $contentHeight ], ], ], @@ -172,6 +177,7 @@ public function testGetConfigIfContentIsNotHtml() $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); $agreement->expects($this->once())->method('getMode')->willReturn($mode); $agreement->expects($this->once())->method('getAgreementId')->willReturn($agreementId); + $agreement->expects($this->once())->method('getContentHeight')->willReturn($contentHeight); $this->assertEquals($expectedResult, $this->model->getConfig()); } diff --git a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml index 09cd1c5b63965..43da8d7d27dd9 100644 --- a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml +++ b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="checkout_agreement" resource="default" engine="innodb" comment="Checkout Agreement"> <column xsi:type="int" name="agreement_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Agreement Id"/> + comment="Agreement ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> <column xsi:type="text" name="content" nullable="true" comment="Content"/> <column xsi:type="varchar" name="content_height" nullable="true" length="25" comment="Content Height"/> @@ -26,9 +26,9 @@ </table> <table name="checkout_agreement_store" resource="default" engine="innodb" comment="Checkout Agreement Store"> <column xsi:type="int" name="agreement_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Agreement Id"/> + comment="Agreement ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="agreement_id"/> <column name="store_id"/> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js index 434676fc04116..a189c42918099 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js @@ -23,6 +23,7 @@ define([ agreements: agreementsConfig.agreements, modalTitle: ko.observable(null), modalContent: ko.observable(null), + contentHeight: ko.observable(null), modalWindow: null, /** @@ -42,6 +43,7 @@ define([ showContent: function (element) { this.modalTitle(element.checkboxText); this.modalContent(element.content); + this.contentHeight(element.contentHeight ? element.contentHeight : 'auto'); agreementsModal.showModal(); }, diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index 4b1a68624e547..f1c807fab3d22 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -35,7 +35,7 @@ <!-- /ko --> <!-- /ko --> <div id="checkout-agreements-modal" data-bind="afterRender: initModal" style="display: none"> - <div class="checkout-agreements-item-content" data-bind="html: modalContent"></div> + <div class="checkout-agreements-item-content" data-bind="html: modalContent, style: {height: contentHeight, overflow:'auto' }"></div> </div> </div> </div> diff --git a/app/code/Magento/Cms/Api/Data/PageInterface.php b/app/code/Magento/Cms/Api/Data/PageInterface.php index 032f4916a85e4..7a31ab1b9a94f 100644 --- a/app/code/Magento/Cms/Api/Data/PageInterface.php +++ b/app/code/Magento/Cms/Api/Data/PageInterface.php @@ -125,6 +125,7 @@ public function getSortOrder(); * Get layout update xml * * @return string|null + * @deprecated Existing updates are applied, new are not accepted. */ public function getLayoutUpdateXml(); @@ -145,6 +146,8 @@ public function getCustomRootTemplate(); /** * Get custom layout update xml * + * @deprecated Existing updates are applied, new are not accepted. + * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface * @return string|null */ public function getCustomLayoutUpdateXml(); @@ -272,6 +275,7 @@ public function setSortOrder($sortOrder); * * @param string $layoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface + * @deprecated Existing updates are applied, new are not accepted. */ public function setLayoutUpdateXml($layoutUpdateXml); @@ -296,6 +300,8 @@ public function setCustomRootTemplate($customRootTemplate); * * @param string $customLayoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface + * @deprecated Existing updates are applied, new are not accepted. + * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface */ public function setCustomLayoutUpdateXml($customLayoutUpdateXml); diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php index e94992ae26b60..9efd24e5003ca 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Block\Adminhtml\Wysiwyg\Images; /** @@ -45,8 +47,12 @@ protected function _construct() $this->buttonList->remove('edit'); $this->buttonList->add( 'cancel', - ['class' => 'cancel action-quaternary', 'label' => __('Cancel'), 'type' => 'button', - 'onclick' => 'MediabrowserUtility.closeDialog();'], + [ + 'class' => 'cancel action-quaternary', + 'label' => __('Cancel'), + 'type' => 'button', + 'onclick' => 'MediabrowserUtility.closeDialog();' + ], 0, 0, 'header' @@ -54,7 +60,11 @@ protected function _construct() $this->buttonList->add( 'delete_folder', - ['class' => 'delete no-display action-quaternary', 'label' => __('Delete Folder'), 'type' => 'button'], + [ + 'class' => 'delete no-display action-quaternary', + 'label' => __('Delete Folder'), + 'type' => 'button' + ], 0, 0, 'header' @@ -62,7 +72,11 @@ protected function _construct() $this->buttonList->add( 'delete_files', - ['class' => 'delete no-display action-quaternary', 'label' => __('Delete Selected'), 'type' => 'button'], + [ + 'class' => 'delete no-display action-quaternary', + 'label' => __('Delete Selected'), + 'type' => 'button' + ], 0, 0, 'header' @@ -70,7 +84,11 @@ protected function _construct() $this->buttonList->add( 'new_folder', - ['class' => 'save', 'label' => __('Create Folder'), 'type' => 'button'], + [ + 'class' => 'save new_folder', + 'label' => __('Create Folder'), + 'type' => 'button' + ], 0, 0, 'header' @@ -78,7 +96,11 @@ protected function _construct() $this->buttonList->add( 'insert_files', - ['class' => 'save no-display action-primary', 'label' => __('Add Selected'), 'type' => 'button'], + [ + 'class' => 'save no-display action-primary', + 'label' => __('Add Selected'), + 'type' => 'button' + ], 0, 0, 'header' @@ -92,9 +114,7 @@ protected function _construct() */ public function getContentsUrl() { - return $this->getUrl('cms/*/contents', [ - 'type' => $this->getRequest()->getParam('type'), - ]); + return $this->getUrl('cms/*/contents', ['type' => $this->getRequest()->getParam('type')]); } /** diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index c611f4b1e9f05..86cf059525e1e 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -13,6 +13,11 @@ */ class Block extends AbstractBlock implements \Magento\Framework\DataObject\IdentityInterface { + /** + * Prefix for cache key of CMS block + */ + const CACHE_KEY_PREFIX = 'CMS_BLOCK_'; + /** * @var \Magento\Cms\Model\Template\FilterProvider */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php index 8774d7e69adfe..2237f35ed0b84 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php @@ -7,6 +7,7 @@ use Magento\Backend\App\Action\Context; use Magento\Cms\Api\PageRepositoryInterface as PageRepository; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Cms\Api\Data\PageInterface; @@ -15,7 +16,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class InlineEdit extends \Magento\Backend\App\Action +class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -56,6 +57,8 @@ public function __construct( } /** + * Process the request + * * @return \Magento\Framework\Controller\ResultInterface * @throws \Magento\Framework\Exception\LocalizedException */ @@ -68,10 +71,12 @@ public function execute() $postItems = $this->getRequest()->getParam('items', []); if (!($this->getRequest()->getParam('isAjax') && count($postItems))) { - return $resultJson->setData([ - 'messages' => [__('Please correct the data sent.')], - 'error' => true, - ]); + return $resultJson->setData( + [ + 'messages' => [__('Please correct the data sent.')], + 'error' => true, + ] + ); } foreach (array_keys($postItems) as $pageId) { @@ -98,10 +103,12 @@ public function execute() } } - return $resultJson->setData([ - 'messages' => $messages, - 'error' => $error - ]); + return $resultJson->setData( + [ + 'messages' => $messages, + 'error' => $error + ] + ); } /** @@ -131,7 +138,7 @@ protected function filterPost($postData = []) */ protected function validatePost(array $pageData, \Magento\Cms\Model\Page $page, &$error, array &$messages) { - if (!($this->dataProcessor->validate($pageData) && $this->dataProcessor->validateRequireEntry($pageData))) { + if (!$this->dataProcessor->validateRequireEntry($pageData)) { $error = true; foreach ($this->messageManager->getMessages(true)->getItems() as $error) { $messages[] = $this->getErrorWithPageId($page, $error->getText()); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php index 9b8933c8dba2e..f1862026f0e35 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php @@ -12,8 +12,7 @@ use Magento\Framework\Config\Dom\ValidationSchemaException; /** - * Class PostDataProcessor - * @package Magento\Cms\Controller\Adminhtml\Page + * Controller helper for user input. */ class PostDataProcessor { @@ -80,6 +79,7 @@ public function filter($data) * * @param array $data * @return bool Return FALSE if some item is invalid + * @deprecated */ public function validate($data) { @@ -140,7 +140,7 @@ private function validateData($data, $layoutXmlValidator) if (!empty($data['layout_update_xml']) && !$layoutXmlValidator->isValid($data['layout_update_xml'])) { return false; } - + if (!empty($data['custom_layout_update_xml']) && !$layoutXmlValidator->isValid($data['custom_layout_update_xml']) ) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php index 37cb45753174f..449fdb4224a57 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php @@ -8,11 +8,14 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Cms\Model\Page; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Exception\LocalizedException; /** * Save CMS page action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface { @@ -59,11 +62,9 @@ public function __construct( ) { $this->dataProcessor = $dataProcessor; $this->dataPersistor = $dataPersistor; - $this->pageFactory = $pageFactory - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Cms\Model\PageFactory::class); + $this->pageFactory = $pageFactory ?: ObjectManager::getInstance()->get(\Magento\Cms\Model\PageFactory::class); $this->pageRepository = $pageRepository - ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Cms\Api\PageRepositoryInterface::class); + ?: ObjectManager::getInstance()->get(\Magento\Cms\Api\PageRepositoryInterface::class); parent::__construct($context); } @@ -100,24 +101,22 @@ public function execute() } } + $data['layout_update_xml'] = $model->getLayoutUpdateXml(); + $data['custom_layout_update_xml'] = $model->getCustomLayoutUpdateXml(); $model->setData($data); - $this->_eventManager->dispatch( - 'cms_page_prepare_save', - ['page' => $model, 'request' => $this->getRequest()] - ); - - if (!$this->dataProcessor->validate($data)) { - return $resultRedirect->setPath('*/*/edit', ['page_id' => $model->getId(), '_current' => true]); - } - try { + $this->_eventManager->dispatch( + 'cms_page_prepare_save', + ['page' => $model, 'request' => $this->getRequest()] + ); + $this->pageRepository->save($model); $this->messageManager->addSuccessMessage(__('You saved the page.')); return $this->processResultRedirect($model, $resultRedirect, $data); } catch (LocalizedException $e) { $this->messageManager->addExceptionMessage($e->getPrevious() ?: $e); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the page.')); } diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php index 70e9437235ac3..39b292bf07239 100644 --- a/app/code/Magento/Cms/Helper/Page.php +++ b/app/code/Magento/Cms/Helper/Page.php @@ -5,7 +5,12 @@ */ namespace Magento\Cms\Helper; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; +use Magento\Cms\Model\Page\CustomLayoutRepositoryInterface; +use Magento\Cms\Model\Page\IdentityMap; use Magento\Framework\App\Action\Action; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; /** * CMS Page Helper @@ -76,6 +81,21 @@ class Page extends \Magento\Framework\App\Helper\AbstractHelper */ protected $resultPageFactory; + /** + * @var CustomLayoutManagerInterface + */ + private $customLayoutManager; + + /** + * @var CustomLayoutRepositoryInterface + */ + private $customLayoutRepo; + + /** + * @var IdentityMap + */ + private $identityMap; + /** * Constructor * @@ -88,6 +108,9 @@ class Page extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\Escaper $escaper * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param CustomLayoutManagerInterface|null $customLayoutManager + * @param CustomLayoutRepositoryInterface|null $customLayoutRepo + * @param IdentityMap|null $identityMap * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +122,10 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Escaper $escaper, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + ?CustomLayoutManagerInterface $customLayoutManager = null, + ?CustomLayoutRepositoryInterface $customLayoutRepo = null, + ?IdentityMap $identityMap = null ) { $this->messageManager = $messageManager; $this->_page = $page; @@ -109,6 +135,11 @@ public function __construct( $this->_localeDate = $localeDate; $this->_escaper = $escaper; $this->resultPageFactory = $resultPageFactory; + $this->customLayoutManager = $customLayoutManager + ?? ObjectManager::getInstance()->get(CustomLayoutManagerInterface::class); + $this->customLayoutRepo = $customLayoutRepo + ?? ObjectManager::getInstance()->get(CustomLayoutRepositoryInterface::class); + $this->identityMap = $identityMap ?? ObjectManager::getInstance()->get(IdentityMap::class); parent::__construct($context); } @@ -136,6 +167,7 @@ public function prepareResultPage(Action $action, $pageId = null) if (!$this->_page->getId()) { return false; } + $this->identityMap->add($this->_page); $inRange = $this->_localeDate->isScopeDateInInterval( null, @@ -152,7 +184,19 @@ public function prepareResultPage(Action $action, $pageId = null) $resultPage = $this->resultPageFactory->create(); $this->setLayoutType($inRange, $resultPage); $resultPage->addHandle('cms_page_view'); - $resultPage->addPageLayoutHandles(['id' => str_replace('/', '_', $this->_page->getIdentifier())]); + $pageHandles = ['id' => str_replace('/', '_', $this->_page->getIdentifier())]; + //Selected custom updates. + try { + $this->customLayoutManager->applyUpdate( + $resultPage, + $this->customLayoutRepo->getFor($this->_page->getId()) + ); + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (NoSuchEntityException $exception) { + //No custom layout selected + } + + $resultPage->addPageLayoutHandles($pageHandles); $this->_eventManager->dispatch( 'cms_page_render', diff --git a/app/code/Magento/Cms/Model/BlockSearchResults.php b/app/code/Magento/Cms/Model/BlockSearchResults.php new file mode 100644 index 0000000000000..2fa5dbb40139e --- /dev/null +++ b/app/code/Magento/Cms/Model/BlockSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model; + +use Magento\Cms\Api\Data\BlockSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Block search results. + */ +class BlockSearchResults extends SearchResults implements BlockSearchResultsInterface +{ +} diff --git a/app/code/Magento/Cms/Model/Page.php b/app/code/Magento/Cms/Model/Page.php index 8eefe26236ba5..28d013f45f1fa 100644 --- a/app/code/Magento/Cms/Model/Page.php +++ b/app/code/Magento/Cms/Model/Page.php @@ -7,7 +7,9 @@ use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Helper\Page as PageHelper; +use Magento\Cms\Model\Page\CustomLayout\CustomLayoutRepository; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; @@ -57,6 +59,32 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface */ private $scopeConfig; + /** + * @var CustomLayoutRepository + */ + private $customLayoutRepository; + + /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data + * @param CustomLayoutRepository|null $customLayoutRepository + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [], + ?CustomLayoutRepository $customLayoutRepository = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); + $this->customLayoutRepository = $customLayoutRepository + ?? ObjectManager::getInstance()->get(CustomLayoutRepository::class); + } + /** * Initialize resource model * @@ -536,34 +564,56 @@ public function setIsActive($isActive) } /** - * @inheritdoc - * @since 101.0.0 + * Validate identifier before saving the entity. + * + * @return void + * @throws LocalizedException */ - public function beforeSave() + private function validateNewIdentifier(): void { $originalIdentifier = $this->getOrigData('identifier'); $currentIdentifier = $this->getIdentifier(); + if ($this->getId() && $originalIdentifier !== $currentIdentifier) { + switch ($originalIdentifier) { + case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_ROUTE_PAGE): + throw new LocalizedException( + __('This identifier is reserved for "CMS No Route Page" in configuration.') + ); + case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_HOME_PAGE): + throw new LocalizedException( + __('This identifier is reserved for "CMS Home Page" in configuration.') + ); + case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_COOKIES_PAGE): + throw new LocalizedException( + __('This identifier is reserved for "CMS No Cookies Page" in configuration.') + ); + } + } + } + /** + * @inheritdoc + * @since 101.0.0 + */ + public function beforeSave() + { if ($this->hasDataChanges()) { $this->setUpdateTime(null); } - if (!$this->getId() || $originalIdentifier === $currentIdentifier) { - return parent::beforeSave(); - } + $this->validateNewIdentifier(); - switch ($originalIdentifier) { - case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_ROUTE_PAGE): - throw new LocalizedException( - __('This identifier is reserved for "CMS No Route Page" in configuration.') - ); - case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_HOME_PAGE): - throw new LocalizedException(__('This identifier is reserved for "CMS Home Page" in configuration.')); - case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_COOKIES_PAGE): - throw new LocalizedException( - __('This identifier is reserved for "CMS No Cookies Page" in configuration.') - ); + //Removing deprecated custom layout update if a new value is provided + $layoutUpdate = $this->getData('layout_update_selected'); + if ($layoutUpdate === '_no_update_' || ($layoutUpdate && $layoutUpdate !== '_existing_')) { + $this->setCustomLayoutUpdateXml(null); + $this->setLayoutUpdateXml(null); + } + if ($layoutUpdate === '_no_update_' || $layoutUpdate === '_existing_') { + $layoutUpdate = null; } + $this->setData('layout_update_selected', $layoutUpdate); + $this->customLayoutRepository->validateLayoutSelectedFor($this); return parent::beforeSave(); } diff --git a/app/code/Magento/Cms/Model/Page/Authorization.php b/app/code/Magento/Cms/Model/Page/Authorization.php new file mode 100644 index 0000000000000..9075141ce15b5 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/Authorization.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use \Magento\Store\Model\StoreManagerInterface; + +/** + * Authorization for saving a page. + */ +class Authorization +{ + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param PageRepositoryInterface $pageRepository + * @param AuthorizationInterface $authorization + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + */ + public function __construct( + PageRepositoryInterface $pageRepository, + AuthorizationInterface $authorization, + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager + ) { + $this->pageRepository = $pageRepository; + $this->authorization = $authorization; + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; + } + + /** + * Check whether the design fields have been changed. + * + * @param PageInterface $page + * @param PageInterface|null $oldPage + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function hasPageChanged(PageInterface $page, ?PageInterface $oldPage): bool + { + if (!$oldPage) { + $oldPageLayout = $this->scopeConfig->getValue( + 'web/default_layouts/default_cms_layout', + ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore() + ); + if ($page->getPageLayout() && $page->getPageLayout() !== $oldPageLayout) { + //If page layout is set and it's not a default value - design attributes are changed. + return true; + } + //Otherwise page layout is empty and is OK to save. + $oldPageLayout = $page->getPageLayout(); + } else { + //Compare page layout to saved value. + $oldPageLayout = $oldPage->getPageLayout(); + } + //Compare new values to saved values or require them to be empty + $oldUpdateXml = $oldPage ? $oldPage->getLayoutUpdateXml() : null; + $oldCustomTheme = $oldPage ? $oldPage->getCustomTheme() : null; + $oldLayoutUpdate = $oldPage ? $oldPage->getCustomLayoutUpdateXml() : null; + $oldThemeFrom = $oldPage ? $oldPage->getCustomThemeFrom() : null; + $oldThemeTo = $oldPage ? $oldPage->getCustomThemeTo() : null; + + if ($page->getLayoutUpdateXml() != $oldUpdateXml + || $page->getPageLayout() != $oldPageLayout + || $page->getCustomTheme() != $oldCustomTheme + || $page->getCustomLayoutUpdateXml() != $oldLayoutUpdate + || $page->getCustomThemeFrom() != $oldThemeFrom + || $page->getCustomThemeTo() != $oldThemeTo + ) { + return true; + } + + return false; + } + + /** + * Authorize user before updating a page. + * + * @param PageInterface $page + * @return void + * @throws AuthorizationException + * @throws \Magento\Framework\Exception\LocalizedException When it is impossible to perform authorization. + */ + public function authorizeFor(PageInterface $page): void + { + //Validate design changes. + if (!$this->authorization->isAllowed('Magento_Cms::save_design')) { + $oldPage = null; + if ($page->getId()) { + $oldPage = $this->pageRepository->getById($page->getId()); + } + if ($this->hasPageChanged($page, $oldPage)) { + throw new AuthorizationException( + __('You are not allowed to change CMS pages design settings') + ); + } + } + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutManager.php b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutManager.php new file mode 100644 index 0000000000000..988bd5b4ac136 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutManager.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; +use Magento\Cms\Model\Page\IdentityMap; +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Result\Page as PageLayout; +use Magento\Framework\View\Model\Layout\Merge as LayoutProcessor; +use Magento\Framework\View\Model\Layout\MergeFactory as LayoutProcessorFactory; + +/** + * @inheritDoc + */ +class CustomLayoutManager implements CustomLayoutManagerInterface +{ + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var LayoutProcessorFactory + */ + private $layoutProcessorFactory; + + /** + * @var LayoutProcessor|null + */ + private $layoutProcessor; + + /** + * @var IdentityMap + */ + private $identityMap; + + /** + * @param FlyweightFactory $themeFactory + * @param DesignInterface $design + * @param PageRepositoryInterface $pageRepository + * @param LayoutProcessorFactory $layoutProcessorFactory + * @param IdentityMap $identityMap + */ + public function __construct( + FlyweightFactory $themeFactory, + DesignInterface $design, + PageRepositoryInterface $pageRepository, + LayoutProcessorFactory $layoutProcessorFactory, + IdentityMap $identityMap + ) { + $this->themeFactory = $themeFactory; + $this->design = $design; + $this->pageRepository = $pageRepository; + $this->layoutProcessorFactory = $layoutProcessorFactory; + $this->identityMap = $identityMap; + } + + /** + * Adopt page's identifier to be used as layout handle. + * + * @param PageInterface $page + * @return string + */ + private function sanitizeIdentifier(PageInterface $page): string + { + return str_replace('/', '_', $page->getIdentifier()); + } + + /** + * Get the processor instance. + * + * @return LayoutProcessor + */ + private function getLayoutProcessor(): LayoutProcessor + { + if (!$this->layoutProcessor) { + $this->layoutProcessor = $this->layoutProcessorFactory->create( + [ + 'theme' => $this->themeFactory->create( + $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND) + ) + ] + ); + $this->themeFactory = null; + $this->design = null; + } + + return $this->layoutProcessor; + } + + /** + * @inheritDoc + */ + public function fetchAvailableFiles(PageInterface $page): array + { + $identifier = $this->sanitizeIdentifier($page); + $handles = $this->getLayoutProcessor()->getAvailableHandles(); + + return array_filter( + array_map( + function (string $handle) use ($identifier) : ?string { + preg_match( + '/^cms\_page\_view\_selectable\_' .preg_quote($identifier) .'\_([a-z0-9]+)/i', + $handle, + $selectable + ); + if (!empty($selectable[1])) { + return $selectable[1]; + } + + return null; + }, + $handles + ) + ); + } + + /** + * @inheritDoc + */ + public function applyUpdate(PageLayout $layout, CustomLayoutSelectedInterface $layoutSelected): void + { + $page = $this->identityMap->get($layoutSelected->getPageId()); + if (!$page) { + $page = $this->pageRepository->getById($layoutSelected->getPageId()); + } + + $layout->addPageLayoutHandles( + ['selectable' => $this->sanitizeIdentifier($page) .'_' .$layoutSelected->getLayoutFileId()] + ); + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutRepository.php b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutRepository.php new file mode 100644 index 0000000000000..cf0db6eb27cb8 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutRepository.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout; + +use Magento\Cms\Model\Page as PageModel; +use Magento\Cms\Model\PageFactory as PageModelFactory; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelected; +use Magento\Cms\Model\Page\CustomLayoutRepositoryInterface; +use Magento\Cms\Model\Page\IdentityMap; +use Magento\Cms\Model\ResourceModel\Page; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; + +/** + * @inheritDoc + */ +class CustomLayoutRepository implements CustomLayoutRepositoryInterface +{ + /** + * @var Page + */ + private $pageRepository; + + /** + * @var PageModelFactory; + */ + private $pageFactory; + + /** + * @var IdentityMap + */ + private $identityMap; + + /** + * @var CustomLayoutManagerInterface + */ + private $manager; + + /** + * @param Page $pageRepository + * @param PageModelFactory $factory + * @param IdentityMap $identityMap + * @param CustomLayoutManagerInterface $manager + */ + public function __construct( + Page $pageRepository, + PageModelFactory $factory, + IdentityMap $identityMap, + CustomLayoutManagerInterface $manager + ) { + $this->pageRepository = $pageRepository; + $this->pageFactory = $factory; + $this->identityMap = $identityMap; + $this->manager = $manager; + } + + /** + * Find page model by ID. + * + * @param int $id + * @return PageModel + * @throws NoSuchEntityException + */ + private function findPage(int $id): PageModel + { + if (!$page = $this->identityMap->get($id)) { + /** @var PageModel $page */ + $this->pageRepository->load($page = $this->pageFactory->create(), $id); + if (!$page->getIdentifier()) { + throw NoSuchEntityException::singleField('id', $id); + } + } + + return $page; + } + + /** + * Check whether the page can use this layout. + * + * @param PageModel $page + * @param string $layoutFile + * @return bool + */ + private function isLayoutValidFor(PageModel $page, string $layoutFile): bool + { + return in_array($layoutFile, $this->manager->fetchAvailableFiles($page), true); + } + + /** + * Save new custom layout file value for a page. + * + * @param int $pageId + * @param string|null $layoutFile + * @throws LocalizedException + * @throws \InvalidArgumentException When invalid file was selected. + * @throws NoSuchEntityException + */ + private function saveLayout(int $pageId, ?string $layoutFile): void + { + $page = $this->findPage($pageId); + if ($layoutFile !== null && !$this->isLayoutValidFor($page, $layoutFile)) { + throw new \InvalidArgumentException( + $layoutFile .' is not available for page #' .$pageId + ); + } + + if ($page->getData('layout_update_selected') != $layoutFile) { + $page->setData('layout_update_selected', $layoutFile); + $this->pageRepository->save($page); + } + } + + /** + * @inheritDoc + */ + public function save(CustomLayoutSelectedInterface $layout): void + { + $this->saveLayout($layout->getPageId(), $layout->getLayoutFileId()); + } + + /** + * Validate layout update of given page model. + * + * @param PageModel $page + * @return void + * @throws LocalizedException + */ + public function validateLayoutSelectedFor(PageModel $page): void + { + $layoutFile = $page->getData('layout_update_selected'); + if ($layoutFile && (!$page->getId() || !$this->isLayoutValidFor($page, $layoutFile))) { + throw new LocalizedException(__('Invalid Custom Layout Update selected')); + } + } + + /** + * @inheritDoc + */ + public function deleteFor(int $pageId): void + { + $this->saveLayout($pageId, null); + } + + /** + * @inheritDoc + */ + public function getFor(int $pageId): CustomLayoutSelectedInterface + { + $page = $this->findPage($pageId); + if (!$page['layout_update_selected']) { + throw new NoSuchEntityException( + __('Page "%id" doesn\'t have custom layout assigned', ['id' => $page->getIdentifier()]) + ); + } + + return new CustomLayoutSelected($pageId, $page['layout_update_selected']); + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelected.php b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelected.php new file mode 100644 index 0000000000000..fc29ebd72e802 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelected.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout\Data; + +/** + * @inheritDoc + */ +class CustomLayoutSelected implements CustomLayoutSelectedInterface +{ + /** + * @var int + */ + private $pageId; + + /** + * @var string + */ + private $layoutFile; + + /** + * @param int $pageId + * @param string $layoutFile + */ + public function __construct(int $pageId, string $layoutFile) + { + $this->pageId = $pageId; + $this->layoutFile = $layoutFile; + } + + /** + * @inheritDoc + */ + public function getPageId(): int + { + return $this->pageId; + } + + /** + * @inheritDoc + */ + public function getLayoutFileId(): string + { + return $this->layoutFile; + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelectedInterface.php b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelectedInterface.php new file mode 100644 index 0000000000000..68bac57e98d56 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelectedInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout\Data; + +/** + * Custom layout update file to be used for the specific CMS page. + */ +interface CustomLayoutSelectedInterface +{ + /** + * CMS page ID. + * + * @return int + */ + public function getPageId(): int; + + /** + * Custom layout file ID (layout update handle value). + * + * @return string + */ + public function getLayoutFileId(): string; +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayoutManagerInterface.php b/app/code/Magento/Cms/Model/Page/CustomLayoutManagerInterface.php new file mode 100644 index 0000000000000..6f15fcef7f8f4 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayoutManagerInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Framework\View\Result\Page as View; + +/** + * Manage custom layout files for CMS pages. + */ +interface CustomLayoutManagerInterface +{ + /** + * List of available custom files for the given page. + * + * @param PageInterface $page + * @return string[] + */ + public function fetchAvailableFiles(PageInterface $page): array; + + /** + * Apply the page's layout settings. + * + * @param View $layout + * @param CustomLayoutSelectedInterface $layoutSelected + * @return void + */ + public function applyUpdate(View $layout, CustomLayoutSelectedInterface $layoutSelected): void; +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayoutRepositoryInterface.php b/app/code/Magento/Cms/Model/Page/CustomLayoutRepositoryInterface.php new file mode 100644 index 0000000000000..80eb39b7ab20f --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayoutRepositoryInterface.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Access to "custom layout" page property. + */ +interface CustomLayoutRepositoryInterface +{ + + /** + * Save layout file to be used when rendering given page. + * + * @throws LocalizedException When failed to save new value. + * @throws \InvalidArgumentException When invalid file was selected. + * @throws NoSuchEntityException When given page is not found. + * @param CustomLayoutSelectedInterface $layout + * @return void + */ + public function save(CustomLayoutSelectedInterface $layout): void; + + /** + * Do not use custom layout update when rendering the page. + * + * @throws NoSuchEntityException When given page is not found. + * @throws LocalizedException When failed to remove existing value. + * @param int $pageId + * @return void + */ + public function deleteFor(int $pageId): void; + + /** + * Find custom layout settings for a page. + * + * @param int $pageId + * @return CustomLayoutSelectedInterface + * @throws NoSuchEntityException When either the page or any settings are found. + */ + public function getFor(int $pageId): CustomLayoutSelectedInterface; +} diff --git a/app/code/Magento/Cms/Model/Page/DataProvider.php b/app/code/Magento/Cms/Model/Page/DataProvider.php index 64abaffd04e66..41010575a1f27 100644 --- a/app/code/Magento/Cms/Model/Page/DataProvider.php +++ b/app/code/Magento/Cms/Model/Page/DataProvider.php @@ -5,9 +5,11 @@ */ namespace Magento\Cms\Model\Page; +use Magento\Cms\Model\Page; use Magento\Cms\Model\ResourceModel\Page\CollectionFactory; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\App\RequestInterface; use Magento\Ui\DataProvider\Modifier\PoolInterface; use Magento\Framework\AuthorizationInterface; @@ -36,6 +38,21 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider */ private $auth; + /** + * @var RequestInterface + */ + private $request; + + /** + * @var CustomLayoutManagerInterface + */ + private $customLayoutManager; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + /** * @param string $name * @param string $primaryFieldName @@ -46,6 +63,9 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider * @param array $data * @param PoolInterface|null $pool * @param AuthorizationInterface|null $auth + * @param RequestInterface|null $request + * @param CustomLayoutManagerInterface|null $customLayoutManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -56,13 +76,35 @@ public function __construct( array $meta = [], array $data = [], PoolInterface $pool = null, - ?AuthorizationInterface $auth = null + ?AuthorizationInterface $auth = null, + ?RequestInterface $request = null, + ?CustomLayoutManagerInterface $customLayoutManager = null ) { $this->collection = $pageCollectionFactory->create(); + $this->collectionFactory = $pageCollectionFactory; $this->dataPersistor = $dataPersistor; parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); $this->meta = $this->prepareMeta($this->meta); + $this->request = $request ?? ObjectManager::getInstance()->get(RequestInterface::class); + $this->customLayoutManager = $customLayoutManager + ?? ObjectManager::getInstance()->get(CustomLayoutManagerInterface::class); + } + + /** + * Find requested page. + * + * @return Page|null + */ + private function findCurrentPage(): ?Page + { + if ($this->getRequestFieldName() && ($pageId = (int)$this->request->getParam($this->getRequestFieldName()))) { + //Loading data for the collection. + $this->getData(); + return $this->collection->getItemById($pageId); + } + + return null; } /** @@ -86,10 +128,15 @@ public function getData() if (isset($this->loadedData)) { return $this->loadedData; } + $this->collection = $this->collectionFactory->create(); $items = $this->collection->getItems(); /** @var $page \Magento\Cms\Model\Page */ foreach ($items as $page) { $this->loadedData[$page->getId()] = $page->getData(); + if ($page->getCustomLayoutUpdateXml() || $page->getLayoutUpdateXml()) { + //Deprecated layout update exists. + $this->loadedData[$page->getId()]['layout_update_selected'] = '_existing_'; + } } $data = $this->dataPersistor->get('cms_page'); @@ -97,6 +144,9 @@ public function getData() $page = $this->collection->getNewEmptyItem(); $page->setData($data); $this->loadedData[$page->getId()] = $page->getData(); + if ($page->getCustomLayoutUpdateXml() || $page->getLayoutUpdateXml()) { + $this->loadedData[$page->getId()]['layout_update_selected'] = '_existing_'; + } $this->dataPersistor->clear('cms_page'); } @@ -134,6 +184,31 @@ public function getMeta() $meta = array_merge_recursive($meta, $designMeta); } + //List of custom layout files available for current page. + $options = [['label' => 'No update', 'value' => '_no_update_']]; + if ($page = $this->findCurrentPage()) { + //We must have a specific page selected. + //If custom layout XML is set then displaying this special option. + if ($page->getCustomLayoutUpdateXml() || $page->getLayoutUpdateXml()) { + $options[] = ['label' => 'Use existing layout update XML', 'value' => '_existing_']; + } + foreach ($this->customLayoutManager->fetchAvailableFiles($page) as $layoutFile) { + $options[] = ['label' => $layoutFile, 'value' => $layoutFile]; + } + } + $customLayoutMeta = [ + 'design' => [ + 'children' => [ + 'custom_layout_update_select' => [ + 'arguments' => [ + 'data' => ['options' => $options] + ] + ] + ] + ] + ]; + $meta = array_merge_recursive($meta, $customLayoutMeta); + return $meta; } } diff --git a/app/code/Magento/Cms/Model/Page/IdentityMap.php b/app/code/Magento/Cms/Model/Page/IdentityMap.php new file mode 100644 index 0000000000000..249010fbf90ce --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/IdentityMap.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Model\Page; + +/** + * Identity map of loaded pages. + */ +class IdentityMap +{ + /** + * @var Page[] + */ + private $pages = []; + + /** + * Add a page to the list. + * + * @param Page $page + * @throws \InvalidArgumentException When page doesn't have an ID. + * @return void + */ + public function add(Page $page): void + { + if (!$page->getId()) { + throw new \InvalidArgumentException('Cannot add non-persisted page to identity map'); + } + $this->pages[$page->getId()] = $page; + } + + /** + * Find a loaded page by ID. + * + * @param int $id + * @return Page|null + */ + public function get(int $id): ?Page + { + if (array_key_exists($id, $this->pages)) { + return $this->pages[$id]; + } + + return null; + } + + /** + * Remove the page from the list. + * + * @param int $id + * @return void + */ + public function remove(int $id): void + { + unset($this->pages[$id]); + } + + /** + * Clear the list. + * + * @return void + */ + public function clear(): void + { + $this->pages = []; + } +} diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index e6777659d7d88..72f07771f59d4 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -8,6 +8,7 @@ use Magento\Cms\Api\Data; use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page\IdentityMap; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\App\ObjectManager; @@ -18,8 +19,6 @@ use Magento\Cms\Model\ResourceModel\Page as ResourcePage; use Magento\Cms\Model\ResourceModel\Page\CollectionFactory as PageCollectionFactory; use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\AuthorizationInterface; -use Magento\Authorization\Model\UserContextInterface; /** * Class PageRepository @@ -73,14 +72,9 @@ class PageRepository implements PageRepositoryInterface private $collectionProcessor; /** - * @var UserContextInterface + * @var IdentityMap */ - private $userContext; - - /** - * @var AuthorizationInterface - */ - private $authorization; + private $identityMap; /** * @param ResourcePage $resource @@ -92,6 +86,7 @@ class PageRepository implements PageRepositoryInterface * @param DataObjectProcessor $dataObjectProcessor * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor + * @param IdentityMap|null $identityMap * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -103,7 +98,8 @@ public function __construct( DataObjectHelper $dataObjectHelper, DataObjectProcessor $dataObjectProcessor, StoreManagerInterface $storeManager, - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor = null, + ?IdentityMap $identityMap = null ) { $this->resource = $resource; $this->pageFactory = $pageFactory; @@ -114,34 +110,31 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->storeManager = $storeManager; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->identityMap = $identityMap ?? ObjectManager::getInstance()->get(IdentityMap::class); } /** - * Get user context. + * Validate new layout update values. * - * @return UserContextInterface + * @param Data\PageInterface $page + * @return void + * @throws \InvalidArgumentException */ - private function getUserContext(): UserContextInterface + private function validateLayoutUpdate(Data\PageInterface $page): void { - if (!$this->userContext) { - $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class); + //Persisted data + $savedPage = $page->getId() ? $this->getById($page->getId()) : null; + //Custom layout update can be removed or kept as is. + if ($page->getCustomLayoutUpdateXml() + && (!$savedPage || $page->getCustomLayoutUpdateXml() !== $savedPage->getCustomLayoutUpdateXml()) + ) { + throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); } - - return $this->userContext; - } - - /** - * Get authorization service. - * - * @return AuthorizationInterface - */ - private function getAuthorization(): AuthorizationInterface - { - if (!$this->authorization) { - $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class); + if ($page->getLayoutUpdateXml() + && (!$savedPage || $page->getLayoutUpdateXml() !== $savedPage->getLayoutUpdateXml()) + ) { + throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); } - - return $this->authorization; } /** @@ -158,33 +151,9 @@ public function save(\Magento\Cms\Api\Data\PageInterface $page) $page->setStoreId($storeId); } try { - //Validate changing of design. - $userType = $this->getUserContext()->getUserType(); - if (( - $userType === UserContextInterface::USER_TYPE_ADMIN - || $userType === UserContextInterface::USER_TYPE_INTEGRATION - ) - && !$this->getAuthorization()->isAllowed('Magento_Cms::save_design') - ) { - if (!$page->getId()) { - $page->setLayoutUpdateXml(null); - $page->setPageLayout(null); - $page->setCustomTheme(null); - $page->setCustomLayoutUpdateXml(null); - $page->setCustomThemeTo(null); - $page->setCustomThemeFrom(null); - } else { - $savedPage = $this->getById($page->getId()); - $page->setLayoutUpdateXml($savedPage->getLayoutUpdateXml()); - $page->setPageLayout($savedPage->getPageLayout()); - $page->setCustomTheme($savedPage->getCustomTheme()); - $page->setCustomLayoutUpdateXml($savedPage->getCustomLayoutUpdateXml()); - $page->setCustomThemeTo($savedPage->getCustomThemeTo()); - $page->setCustomThemeFrom($savedPage->getCustomThemeFrom()); - } - } - + $this->validateLayoutUpdate($page); $this->resource->save($page); + $this->identityMap->add($page); } catch (\Exception $exception) { throw new CouldNotSaveException( __('Could not save the page: %1', $exception->getMessage()), @@ -208,6 +177,8 @@ public function getById($pageId) if (!$page->getId()) { throw new NoSuchEntityException(__('The CMS page with the "%1" ID doesn\'t exist.', $pageId)); } + $this->identityMap->add($page); + return $page; } @@ -245,6 +216,7 @@ public function delete(\Magento\Cms\Api\Data\PageInterface $page) { try { $this->resource->delete($page); + $this->identityMap->remove($page->getId()); } catch (\Exception $exception) { throw new CouldNotDeleteException( __('Could not delete the page: %1', $exception->getMessage()) @@ -276,7 +248,7 @@ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - 'Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor' + \Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor::class ); } return $this->collectionProcessor; diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php new file mode 100644 index 0000000000000..9fd94d4c11e1c --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\PageRepository; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaInterface; + +/** + * Validates and saves a page + */ +class ValidationComposite implements PageRepositoryInterface +{ + /** + * @var PageRepositoryInterface + */ + private $repository; + + /** + * @var array + */ + private $validators; + + /** + * @param PageRepositoryInterface $repository + * @param ValidatorInterface[] $validators + */ + public function __construct( + PageRepositoryInterface $repository, + array $validators = [] + ) { + foreach ($validators as $validator) { + if (!$validator instanceof ValidatorInterface) { + throw new \InvalidArgumentException( + sprintf('Supplied validator does not implement %s', ValidatorInterface::class) + ); + } + } + $this->repository = $repository; + $this->validators = $validators; + } + + /** + * @inheritdoc + */ + public function save(PageInterface $page) + { + foreach ($this->validators as $validator) { + $validator->validate($page); + } + + return $this->repository->save($page); + } + + /** + * @inheritdoc + */ + public function getById($pageId) + { + return $this->repository->getById($pageId); + } + + /** + * @inheritdoc + */ + public function getList(SearchCriteriaInterface $searchCriteria) + { + return $this->repository->getList($searchCriteria); + } + + /** + * @inheritdoc + */ + public function delete(PageInterface $page) + { + return $this->repository->delete($page); + } + + /** + * @inheritdoc + */ + public function deleteById($pageId) + { + return $this->repository->deleteById($pageId); + } +} diff --git a/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php b/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php new file mode 100644 index 0000000000000..721fd9efbdd91 --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\PageRepository\Validator; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\PageRepository\ValidatorInterface; +use Magento\Framework\Config\Dom\ValidationException; +use Magento\Framework\Config\Dom\ValidationSchemaException; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Model\Layout\Update\Validator; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; + +/** + * Validate a given page + */ +class LayoutUpdateValidator implements ValidatorInterface +{ + /** + * @var ValidatorFactory + */ + private $validatorFactory; + + /** + * @var ValidationStateInterface + */ + private $validationState; + + /** + * @param ValidatorFactory $validatorFactory + * @param ValidationStateInterface $validationState + */ + public function __construct( + ValidatorFactory $validatorFactory, + ValidationStateInterface $validationState + ) { + $this->validatorFactory = $validatorFactory; + $this->validationState = $validationState; + } + + /** + * Validate the data before saving + * + * @param PageInterface $page + * @throws LocalizedException + */ + public function validate(PageInterface $page): void + { + $this->validateRequiredFields($page); + $this->validateLayoutUpdate($page); + $this->validateCustomLayoutUpdate($page); + } + + /** + * Validate required fields + * + * @param PageInterface $page + * @throws LocalizedException + */ + private function validateRequiredFields(PageInterface $page): void + { + if (empty($page->getTitle())) { + throw new LocalizedException(__('Required field "%1" is empty.', 'title')); + } + } + + /** + * Validate layout update + * + * @param PageInterface $page + * @throws LocalizedException + */ + private function validateLayoutUpdate(PageInterface $page): void + { + $layoutXmlValidator = $this->getLayoutValidator(); + + try { + if (!empty($page->getLayoutUpdateXml()) + && !$layoutXmlValidator->isValid($page->getLayoutUpdateXml()) + ) { + throw new LocalizedException(__('Layout update is invalid')); + } + } catch (ValidationException|ValidationSchemaException $e) { + throw new LocalizedException(__('Layout update is invalid')); + } + } + + /** + * Validate custom layout update + * + * @param PageInterface $page + * @throws LocalizedException + */ + private function validateCustomLayoutUpdate(PageInterface $page): void + { + $layoutXmlValidator = $this->getLayoutValidator(); + + try { + if (!empty($page->getCustomLayoutUpdateXml()) + && !$layoutXmlValidator->isValid($page->getCustomLayoutUpdateXml()) + ) { + throw new LocalizedException(__('Custom layout update is invalid')); + } + } catch (ValidationException|ValidationSchemaException $e) { + throw new LocalizedException(__('Custom layout update is invalid')); + } + } + + /** + * Return a new validator + * + * @return Validator + */ + private function getLayoutValidator(): Validator + { + return $this->validatorFactory->create( + [ + 'validationState' => $this->validationState, + ] + ); + } +} diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php b/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php new file mode 100644 index 0000000000000..ff5c7648a9fa2 --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\PageRepository; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Validate a page repository + */ +interface ValidatorInterface +{ + /** + * Assert the given page valid + * + * @param PageInterface $page + * @return void + * @throws LocalizedException + */ + public function validate(PageInterface $page): void; +} diff --git a/app/code/Magento/Cms/Model/PageSearchResults.php b/app/code/Magento/Cms/Model/PageSearchResults.php new file mode 100644 index 0000000000000..7985e382be273 --- /dev/null +++ b/app/code/Magento/Cms/Model/PageSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model; + +use Magento\Cms\Api\Data\PageSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Page search results. + */ +class PageSearchResults extends SearchResults implements PageSearchResultsInterface +{ +} diff --git a/app/code/Magento/Cms/Observer/PageAclPlugin.php b/app/code/Magento/Cms/Observer/PageAclPlugin.php new file mode 100644 index 0000000000000..c71fe0af396c0 --- /dev/null +++ b/app/code/Magento/Cms/Observer/PageAclPlugin.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Observer; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page\Authorization; + +/** + * Perform additional authorization before saving a page. + */ +class PageAclPlugin +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * Authorize saving before it is executed. + * + * @param PageRepositoryInterface $subject + * @param PageInterface $page + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(PageRepositoryInterface $subject, PageInterface $page): array + { + $this->authorization->authorizeFor($page); + + return [$page]; + } +} diff --git a/app/code/Magento/Cms/Observer/PageValidatorObserver.php b/app/code/Magento/Cms/Observer/PageValidatorObserver.php new file mode 100644 index 0000000000000..b4e5d2bc0e0a7 --- /dev/null +++ b/app/code/Magento/Cms/Observer/PageValidatorObserver.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Observer; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page\Authorization; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Performing additional validation each time a user saves a CMS page. + */ +class PageValidatorObserver implements ObserverInterface +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * @inheritDoc + * + * @throws LocalizedException + */ + public function execute(Observer $observer) + { + /** @var PageInterface $page */ + $page = $observer->getEvent()->getData('page'); + $this->authorization->authorizeFor($page); + } +} diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSBlockContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSBlockContentActionGroup.xml new file mode 100644 index 0000000000000..711a8af38efe3 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSBlockContentActionGroup.xml @@ -0,0 +1,29 @@ +<?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="AdminAddImageToCMSBlockContent"> + <arguments> + <argument name="image" type="entity" defaultValue="MagentoLogo"/> + </arguments> + <click selector="{{TinyMCESection.InsertImage}}" stepKey="clickAddImageButton"/> + <waitForElementVisible selector="{{MediaGallerySection.Browse}}" stepKey="waitForBrowseImage"/> + <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowseImage"/> + <waitForElementVisible selector="{{MediaGallerySection.StorageRootArrow}}" stepKey="waitForAttacheFiles"/> + <waitForLoadingMaskToDisappear stepKey="waitForStorageRootLoadingMaskDisappear"/> + <click selector="{{MediaGallerySection.StorageRootArrow}}" stepKey="clickRoot"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <attachFile selector="{{MediaGallerySection.BrowseUploadImage}}" userInput="{{image.file}}" stepKey="attachLogo"/> + <waitForElementVisible selector="{{MediaGallerySection.InsertFile}}" stepKey="waitForAddSelected"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> + <click selector="{{MediaGallerySection.InsertFile}}" stepKey="clickAddSelected"/> + <waitForElementVisible selector="{{MediaGallerySection.OkBtn}}" stepKey="waitForOkButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> + <click selector="{{MediaGallerySection.OkBtn}}" stepKey="clickOk"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml new file mode 100644 index 0000000000000..7e907b5b395a4 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml @@ -0,0 +1,16 @@ +<?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="AdminOpenCmsPageActionGroup"> + <arguments> + <argument name="page_id" type="string"/> + </arguments> + <amOnPage url="{{AdminCmsPageEditPage.url(page_id)}}" stepKey="openEditCmsPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml index dff2d60c9e8c3..52a5757ec7b9a 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml @@ -18,7 +18,8 @@ <waitForElementVisible selector="{{MediaGallerySection.imageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="waitForInitialImages"/> <grabMultiple selector="{{MediaGallerySection.imageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="initialImages"/> - <click selector="{{MediaGallerySection.imageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="selectImage"/> + <waitForElementVisible selector="{{MediaGallerySection.lastImageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="waitForLastImage"/> + <click selector="{{MediaGallerySection.lastImageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="selectImage"/> <waitForElementVisible selector="{{MediaGallerySection.DeleteSelectedBtn}}" stepKey="waitForDeleteBtn"/> <click selector="{{MediaGallerySection.DeleteSelectedBtn}}" stepKey="clickDeleteSelected"/> <waitForPageLoad stepKey="waitForPageLoad1"/> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml index e2a3bbcbdc797..073c299c240e9 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml @@ -52,7 +52,8 @@ <actionGroup name="attachImage"> <annotations> - <description>Uploads the provided Image to Media Gallery.</description> + <description>Uploads the provided Image to Media Gallery. + If you use this action group, you MUST add steps to delete the image in the "after" steps.</description> </annotations> <arguments> <argument name="Image"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsPageEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsPageEditPage.xml new file mode 100644 index 0000000000000..978b6d6a6d261 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsPageEditPage.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="AdminCmsPageEditPage" area="admin" url="/cms/page/edit/page_id/{{id}}" parameterized="true" module="Magento_Cms"> + <section name="CmsNewPagePageActionsSection"/> + <section name="CmsNewPagePageBasicFieldsSection"/> + <section name="CmsNewPagePageContentSection"/> + <section name="CmsNewPagePageSeoSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml index 2efa7f62fc4ec..445279a8b1403 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml @@ -24,6 +24,8 @@ </section> <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> + <element name="image" type="file" selector="#tinymce img"/> + <element name="contentIframe" type="iframe" selector="cms_block_form_content_ifr"/> </section> <section name="CmsBlockBlockActionSection"> <element name="deleteBlock" type="button" selector="#delete" timeout="30"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml index b1d0faa7507f0..b85c7554b58ae 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml @@ -40,6 +40,7 @@ <element name="BrowseUploadImage" type="file" selector=".fileupload" /> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageOrImageCopy" type="text" selector="//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')]" parameterized="true"/> + <element name="lastImageOrImageCopy" type="text" selector="(//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')])[last()]" parameterized="true"/> <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 5baf75d43c53f..03edc69e6d625 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -16,9 +16,6 @@ <description value="Admin should be able to add image to WYSIWYG content of Block"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84376"/> - <skip> - <issueId value="MC-17232"/> - </skip> </annotations> <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml index e63a6be51bcc0..205850f888797 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml @@ -16,9 +16,6 @@ <description value="Admin should be able to add image to WYSIWYG content of CMS Page"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-85825"/> - <skip> - <issueId value="MC-17232"/> - </skip> </annotations> <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml index fccc5b5980f2b..b7c7e4a4212fe 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml @@ -23,7 +23,7 @@ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> <waitForPageLoad stepKey="waitForPageLoad1"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml new file mode 100644 index 0000000000000..bc1688c9d692b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml @@ -0,0 +1,62 @@ +<?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="AdminMediaGalleryPopupUploadImagesWithoutErrorTest"> + <annotations> + <features value="Cms"/> + <stories value="Spinner is Always Displayed on Media Gallery popup"/> + <title value="Media Gallery popup upload images without error"/> + <description value="Media Gallery popup upload images without error"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-18962"/> + <useCaseId value="MC-18709"/> + <group value="Cms"/> + </annotations> + <before> + <!--Enable WYSIWYG options--> + <comment userInput="Enable WYSIWYG options" stepKey="commentEnableWYSIWYG"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYGEditor"/> + <magentoCLI command="config:set cms/wysiwyg/editor 'TinyMCE 4'" stepKey="setValueWYSIWYGEditor"/> + <!--Create block--> + <comment userInput="Create block" stepKey="commentCreateBlock"/> + <createData entity="Sales25offBlock" stepKey="createBlock"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + </before> + <after> + <!--Disable WYSIWYG options--> + <comment userInput="Disable WYSIWYG options" stepKey="commentDisableWYSIWYG"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createBlock" stepKey="deleteBlock" /> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open created block page and add image--> + <comment userInput="Open create block page and add image" stepKey="commentOpenBlockPage"/> + <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$createBlock$$"/> + </actionGroup> + <actionGroup ref="AdminAddImageToCMSBlockContent" stepKey="addImage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <click selector="{{BlockWYSIWYGSection.ShowHideBtn}}" stepKey="clickShowHideBtnFirstTime"/> + <click selector="{{BlockWYSIWYGSection.ShowHideBtn}}" stepKey="clickShowHideBtnSecondTime"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--Switch to content frame and click on image--> + <comment userInput="Switch to content frame and click on image" stepKey="commentSwitchToIframe"/> + <switchToIFrame selector="{{BlockContentSection.contentIframe}}" stepKey="switchToContentFrame"/> + <click selector="{{BlockContentSection.image}}" stepKey="clickImage"/> + <switchToIFrame stepKey="switchBack"/> + <!--Add image second time and assert--> + <comment userInput="Add image second time and assert" stepKey="commentAddImageAndAssert"/> + <actionGroup ref="AdminAddImageToCMSBlockContent" stepKey="addImageSecondTime"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <switchToIFrame selector="{{BlockContentSection.contentIframe}}" stepKey="switchToContentFrameSecondTime"/> + <seeElement selector="{{BlockContentSection.image}}" stepKey="seeImageElement"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml new file mode 100644 index 0000000000000..ce093144e6a2a --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml @@ -0,0 +1,138 @@ +<?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="CheckOrderOfProdsInWidgetOnCMSPageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Widgets"/> + <title value="Checking order of products in a widget on a CMS page - SKU condition"/> + <description value="Checking order of products in a widget on a CMS page - SKU condition"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13718"/> + <useCaseId value="MC-5906"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> + <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="enableTinyMCE4"/> + <waitForPageLoad stepKey="waitConfigToSave"/> + <createData entity="ApiCategory" stepKey="createFirstCategory"/> + <createData entity="ApiSimpleProduct" stepKey="product1"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product2"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="_defaultCmsPage" stepKey="createCMSPage"/> + </before> + <after> + <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createFirstCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage1"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <conditionalClick selector="{{CmsNewPagePageContentSection.header}}" + dependentSelector="{{CmsNewPagePageContentSection.header}}._show" visible="false" + stepKey="clickContentTab1"/> + <waitForPageLoad stepKey="waitForContentSectionLoad1"/> + <waitForElementNotVisible selector="{{CmsWYSIWYGSection.CheckIfTabExpand}}" stepKey="waitForTabExpand1"/> + <click selector="{{CmsNewPagePageActionsSection.showHideEditor}}" stepKey="showHiddenButtons"/> + <seeElement selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="seeWidgetButton"/> + <click selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="clickInsertWidgetButton"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage"/> + <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" + stepKey="seeInsertWidgetDisabled"/> + <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled"/> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" + stepKey="selectCatalogProductsList"/> + <waitForPageLoad stepKey="waitBeforeClickingOnAddParamBtn"/> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn"/> + <waitForElement selector="{{WidgetSection.ConditionsDropdown}}" stepKey="addingWaitForConditionsDropDown"/> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="SKU" + stepKey="selectCategoryCondition"/> + <waitForPageLoad stepKey="waitBeforeClickingOnRuleParam"/> + <click selector="{{WidgetSection.RuleParam1('3')}}" stepKey="clickOnRuleParam1"/> + <waitForElementVisible selector="{{WidgetSection.RuleParamSelect('1','1')}}" + stepKey="waitDropdownToAppear"/> + <selectOption selector="{{WidgetSection.RuleParamSelect('1','1')}}" userInput="is one of" + stepKey="selectOption"/> + <waitForElement selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParam"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickOnRuleParam"/> + <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement"/> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{WidgetSection.ChooserName}}" userInput="$$product1.name$$" + stepKey="fillProduct1Name"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeClickingOnSearchFilter1"/> + <click selector="{{AdminNewWidgetSection.searchBlock}}" stepKey="searchFilter1"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeSelectingProduct"/> + <click selector="{{WidgetSection.PreCreateProduct('$$product1.name$$')}}" stepKey="selectProduct1"/> + <click selector="{{AdminWidgetsSection.resetFilter}}" stepKey="resetFilter1"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeFillingProductName"/> + <fillField selector="{{WidgetSection.ChooserName}}" userInput="$$product2.name$$" + stepKey="fillProduct2Name"/> + <click selector="{{AdminNewWidgetSection.searchBlock}}" stepKey="clickOnSearch"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeSelectingProduct2"/> + <click selector="{{WidgetSection.PreCreateProduct('$$product2.name$$')}}" stepKey="selectProduct2"/> + <click selector="{{AdminNewWidgetSection.applyParameter}}" stepKey="applyProducts"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickOnInsertWidgetButton"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeClickingOnSaveWidget1"/> + <click selector="{{InsertWidgetSection.save}}" stepKey="saveWidget"/> + <waitForPageLoad stepKey="waitForSaveComplete"/> + <actionGroup ref="CompareTwoProductsOrder" stepKey="compareProductOrders1"> + <argument name="page" value="$$createCMSPage.identifier$$"/> + <argument name="product_1" value="$$product1$$"/> + <argument name="product_2" value="$$product2$$"/> + </actionGroup> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage2"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <conditionalClick selector="{{CmsNewPagePageContentSection.header}}" + dependentSelector="{{CmsNewPagePageContentSection.header}}._show" visible="false" + stepKey="clickContentTab2"/> + <waitForPageLoad stepKey="waitForContentSectionLoad2"/> + <waitForElementNotVisible selector="{{CmsWYSIWYGSection.CheckIfTabExpand}}" stepKey="waitForTabExpand2"/> + <executeJS function="jQuery('[id=\'cms_page_form_content_ifr\']').attr('name', 'preview-iframe')" + stepKey="setPreviewFrameName"/> + <switchToIFrame selector="preview-iframe" stepKey="switchToIframe"/> + <doubleClick selector="{{TinyMCESection.WidgetButton}}" stepKey="clickToEditWidget"/> + <switchToIFrame stepKey="switchOutFromIframe"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeOpeningProductsList"/> + <click selector="{{WidgetSection.RuleParam1('4')}}" stepKey="openProductsList"/> + <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement2"/> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser2"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeFillingProduct1Name"/> + <fillField selector="{{WidgetSection.ChooserName}}" userInput="$$product1.name$$" stepKey="fillProduct1Name_2"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeClickingOnSearchFilter2"/> + <click selector="{{AdminNewWidgetSection.searchBlock}}" stepKey="searchFilter2"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeSelectingProduct1"/> + <click selector="{{WidgetSection.PreCreateProduct('$$product1.name$$')}}" stepKey="selectProduct1_1"/> + <click selector="{{WidgetSection.PreCreateProduct('$$product1.name$$')}}" stepKey="selectProduct2_2"/> + <click selector="{{AdminNewWidgetSection.applyParameter}}" stepKey="applyProducts1"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickOnInsertWidgetButton1"/> + <waitForPageLoad stepKey="waitForPageToLoadBeforeClickingOnSaveWidget2"/> + <click selector="{{InsertWidgetSection.save}}" stepKey="saveWidget1"/> + <waitForPageLoad stepKey="waitForSaveComplete1"/> + + <actionGroup ref="CompareTwoProductsOrder" stepKey="compareProductOrders2"> + <argument name="page" value="$$createCMSPage.identifier$$"/> + <argument name="product_1" value="$$product2$$"/> + <argument name="product_2" value="$$product1$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php index 0d44f66048ba3..c5bb26d04c734 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php @@ -373,6 +373,58 @@ public function testSaveAndClose() $this->assertSame($this->resultRedirect, $this->saveController->execute()); } + public function testSaveActionWithMarginalSpace() + { + $postData = [ + 'title' => 'unique_title_123', + 'identifier' => ' unique_title_123', + 'stores' => ['0'], + 'is_active' => true, + 'content' => '', + 'back' => 'continue' + ]; + + $this->requestMock->expects($this->any())->method('getPostValue')->willReturn($postData); + $this->requestMock->expects($this->atLeastOnce()) + ->method('getParam') + ->willReturnMap( + [ + ['block_id', null, 1], + ['back', null, true], + ] + ); + + $this->blockFactory->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($this->blockMock); + + $this->blockRepository->expects($this->once()) + ->method('getById') + ->with($this->blockId) + ->willReturn($this->blockMock); + + $this->blockMock->expects($this->once())->method('setData'); + $this->blockRepository->expects($this->once())->method('save') + ->with($this->blockMock) + ->willThrowException(new \Exception('No marginal white space please.')); + + $this->messageManagerMock->expects($this->never()) + ->method('addSuccessMessage'); + $this->messageManagerMock->expects($this->once()) + ->method('addExceptionMessage'); + + $this->dataPersistorMock->expects($this->any()) + ->method('set') + ->with('cms_block', array_merge($postData, ['block_id' => null])); + + $this->resultRedirect->expects($this->atLeastOnce()) + ->method('setPath') + ->with('*/*/edit', ['block_id' => $this->blockId]) + ->willReturnSelf(); + + $this->assertSame($this->resultRedirect, $this->saveController->execute()); + } + public function testSaveActionThrowsException() { $postData = [ diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php index 9d51431b26d8f..7f2ff2086df91 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php @@ -102,10 +102,6 @@ public function prepareMocksForTestExecute() ->method('filter') ->with($postData[1]) ->willReturnArgument(0); - $this->dataProcessor->expects($this->once()) - ->method('validate') - ->with($postData[1]) - ->willReturn(false); $this->messageManager->expects($this->once()) ->method('getMessages') ->with(true) @@ -122,19 +118,23 @@ public function prepareMocksForTestExecute() ->willReturn('1'); $this->cmsPage->expects($this->atLeastOnce()) ->method('getData') - ->willReturn([ - 'layout' => '1column', - 'identifier' => 'test-identifier' - ]); + ->willReturn( + [ + 'layout' => '1column', + 'identifier' => 'test-identifier' + ] + ); $this->cmsPage->expects($this->once()) ->method('setData') - ->with([ - 'layout' => '1column', - 'title' => '404 Not Found', - 'identifier' => 'no-route', - 'custom_theme' => '1', - 'custom_root_template' => '2' - ]); + ->with( + [ + 'layout' => '1column', + 'title' => '404 Not Found', + 'identifier' => 'no-route', + 'custom_theme' => '1', + 'custom_root_template' => '2' + ] + ); $this->jsonFactory->expects($this->once()) ->method('create') ->willReturn($this->resultJson); @@ -149,13 +149,15 @@ public function testExecuteWithLocalizedException() ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('LocalizedException'))); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - '[Page ID: 1] Error message', - '[Page ID: 1] LocalizedException' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + '[Page ID: 1] Error message', + '[Page ID: 1] LocalizedException' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); @@ -170,13 +172,15 @@ public function testExecuteWithRuntimeException() ->willThrowException(new \RuntimeException(__('RuntimeException'))); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - '[Page ID: 1] Error message', - '[Page ID: 1] RuntimeException' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + '[Page ID: 1] Error message', + '[Page ID: 1] RuntimeException' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); @@ -191,13 +195,15 @@ public function testExecuteWithException() ->willThrowException(new \Exception(__('Exception'))); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - '[Page ID: 1] Error message', - '[Page ID: 1] Something went wrong while saving the page.' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + '[Page ID: 1] Error message', + '[Page ID: 1] Something went wrong while saving the page.' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); @@ -218,12 +224,14 @@ public function testExecuteWithoutData() ); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - 'Please correct the data sent.' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + 'Please correct the data sent.' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php index 26b4055923107..91adcdd6db4c8 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php @@ -157,6 +157,7 @@ public function testSaveAction() $this->pageRepository->expects($this->once())->method('getById')->with($this->pageId)->willReturn($page); $page->expects($this->once())->method('setData'); + $page->method('getId')->willReturn($this->pageId); $this->pageRepository->expects($this->once())->method('save')->with($page); $this->dataPersistorMock->expects($this->any()) @@ -235,6 +236,7 @@ public function testSaveAndContinue() $page = $this->getMockBuilder(\Magento\Cms\Model\Page::class) ->disableOriginalConstructor() ->getMock(); + $page->method('getId')->willReturn(1); $this->pageFactory->expects($this->atLeastOnce()) ->method('create') ->willReturn($page); @@ -293,7 +295,14 @@ public function testSaveActionThrowsException() $this->dataPersistorMock->expects($this->any()) ->method('set') - ->with('cms_page', ['page_id' => $this->pageId]); + ->with( + 'cms_page', + [ + 'page_id' => $this->pageId, + 'layout_update_xml' => null, + 'custom_layout_update_xml' => null + ] + ); $this->resultRedirect->expects($this->atLeastOnce()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php new file mode 100644 index 0000000000000..f73396230a669 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\PageRepository; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\PageRepository\ValidationComposite; +use Magento\Cms\Model\PageRepository\ValidatorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Exception\LocalizedException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Validate behavior of the validation composite + */ +class ValidationCompositeTest extends TestCase +{ + /** + * @var PageRepositoryInterface|MockObject + */ + private $subject; + + protected function setUp() + { + /** @var PageRepositoryInterface subject */ + $this->subject = $this->createMock(PageRepositoryInterface::class); + } + + /** + * @param $validators + * @expectedException \InvalidArgumentException + * @dataProvider constructorArgumentProvider + */ + public function testConstructorValidation($validators) + { + new ValidationComposite($this->subject, $validators); + } + + public function testSaveInvokesValidatorsWithSucess() + { + $validator1 = $this->createMock(ValidatorInterface::class); + $validator2 = $this->createMock(ValidatorInterface::class); + $page = $this->createMock(PageInterface::class); + + // Assert each are called + $validator1 + ->expects($this->once()) + ->method('validate') + ->with($page); + $validator2 + ->expects($this->once()) + ->method('validate') + ->with($page); + + // Assert that the success is called + $this->subject + ->expects($this->once()) + ->method('save') + ->with($page) + ->willReturn('foo'); + + $composite = new ValidationComposite($this->subject, [$validator1, $validator2]); + $result = $composite->save($page); + + self::assertSame('foo', $result); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Oh no. That isn't right. + */ + public function testSaveInvokesValidatorsWithErrors() + { + $validator1 = $this->createMock(ValidatorInterface::class); + $validator2 = $this->createMock(ValidatorInterface::class); + $page = $this->createMock(PageInterface::class); + + // Assert the first is called + $validator1 + ->expects($this->once()) + ->method('validate') + ->with($page) + ->willThrowException(new LocalizedException(__('Oh no. That isn\'t right.'))); + + // Assert the second is NOT called + $validator2 + ->expects($this->never()) + ->method('validate'); + + // Assert that the success is NOT called + $this->subject + ->expects($this->never()) + ->method('save'); + + $composite = new ValidationComposite($this->subject, [$validator1, $validator2]); + $composite->save($page); + } + + /** + * @param $method + * @param $arg + * @dataProvider passthroughMethodDataProvider + */ + public function testPassthroughMethods($method, $arg) + { + $this->subject + ->method($method) + ->with($arg) + ->willReturn('foo'); + + $composite = new ValidationComposite($this->subject, []); + $result = $composite->{$method}($arg); + + self::assertSame('foo', $result); + } + + public function constructorArgumentProvider() + { + return [ + [[null], false], + [[''], false], + [['foo'], false], + [[new \stdClass()], false], + [[$this->createMock(ValidatorInterface::class), 'foo'], false], + ]; + } + + public function passthroughMethodDataProvider() + { + return [ + ['save', $this->createMock(PageInterface::class)], + ['getById', 1], + ['getList', $this->createMock(SearchCriteriaInterface::class)], + ['delete', $this->createMock(PageInterface::class)], + ['deleteById', 1], + ]; + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php new file mode 100644 index 0000000000000..487a90bb9a185 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\PageRepository\Validator; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator; +use Magento\Framework\Config\Dom\ValidationException; +use Magento\Framework\Config\Dom\ValidationSchemaException; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; +use Magento\Framework\View\Model\Layout\Update\Validator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test cases for the layout update validator + */ +class LayoutUpdateValidatorTest extends TestCase +{ + /** + * @var Validator|MockObject + */ + private $layoutValidator; + + /** + * @var LayoutUpdateValidator + */ + private $validator; + + protected function setUp() + { + $layoutValidatorFactory = $this->createMock(ValidatorFactory::class); + $this->layoutValidator = $this->createMock(Validator::class); + $layoutValidatorState = $this->createMock(ValidationStateInterface::class); + + $layoutValidatorFactory + ->method('create') + ->with(['validationState' => $layoutValidatorState]) + ->willReturn($this->layoutValidator); + + $this->validator = new LayoutUpdateValidator($layoutValidatorFactory, $layoutValidatorState); + } + + /** + * @dataProvider validationSetDataProvider + */ + public function testValidate($data, $expectedExceptionMessage, $layoutValidatorException, $isLayoutValid = false) + { + if ($expectedExceptionMessage) { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + } + + if ($layoutValidatorException) { + $this->layoutValidator + ->method('isValid') + ->with($data['getLayoutUpdateXml'] ?? $data['getCustomLayoutUpdateXml']) + ->willThrowException($layoutValidatorException); + } elseif (!empty($data['getLayoutUpdateXml'])) { + $this->layoutValidator + ->method('isValid') + ->with($data['getLayoutUpdateXml']) + ->willReturn($isLayoutValid); + } elseif (!empty($data['getCustomLayoutUpdateXml'])) { + $this->layoutValidator + ->method('isValid') + ->with($data['getCustomLayoutUpdateXml']) + ->willReturn($isLayoutValid); + } + + $page = $this->createMock(PageInterface::class); + foreach ($data as $method => $value) { + $page + ->method($method) + ->willReturn($value); + } + + self::assertNull($this->validator->validate($page)); + } + + public function validationSetDataProvider() + { + $layoutError = 'Layout update is invalid'; + $customLayoutError = 'Custom layout update is invalid'; + $validationException = new ValidationException('Invalid format'); + $schemaException = new ValidationSchemaException(__('Invalid format')); + + return [ + [['getTitle' => ''], 'Required field "title" is empty.', null], + [['getTitle' => null], 'Required field "title" is empty.', null], + [['getTitle' => false], 'Required field "title" is empty.', null], + [['getTitle' => 0], 'Required field "title" is empty.', null], + [['getTitle' => '0'], 'Required field "title" is empty.', null], + [['getTitle' => []], 'Required field "title" is empty.', null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => ''], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => null], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => false], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 0], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => '0'], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => []], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, $validationException], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, $schemaException], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], null, null, true], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => ''], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => null], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => false], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 0], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => '0'], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => []], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, $validationException], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, $schemaException], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], null, null, true], + ]; + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepositoryTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepositoryTest.php deleted file mode 100644 index 61001794e2a0b..0000000000000 --- a/app/code/Magento/Cms/Test/Unit/Model/PageRepositoryTest.php +++ /dev/null @@ -1,268 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Cms\Test\Unit\Model; - -use Magento\Cms\Model\PageRepository; -use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; - -/** - * Test for Magento\Cms\Model\PageRepository - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class PageRepositoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var PageRepository - */ - protected $repository; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Model\ResourceModel\Page - */ - protected $pageResource; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Model\Page - */ - protected $page; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Api\Data\PageInterface - */ - protected $pageData; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Api\Data\PageSearchResultsInterface - */ - protected $pageSearchResult; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\DataObjectHelper - */ - protected $dataHelper; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Reflection\DataObjectProcessor - */ - protected $dataObjectProcessor; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Model\ResourceModel\Page\Collection - */ - protected $collection; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface - */ - private $storeManager; - - /** - * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $collectionProcessor; - - /** - * Initialize repository - */ - protected function setUp() - { - $this->pageResource = $this->getMockBuilder(\Magento\Cms\Model\ResourceModel\Page::class) - ->disableOriginalConstructor() - ->getMock(); - $this->dataObjectProcessor = $this->getMockBuilder(\Magento\Framework\Reflection\DataObjectProcessor::class) - ->disableOriginalConstructor() - ->getMock(); - $pageFactory = $this->getMockBuilder(\Magento\Cms\Model\PageFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $pageDataFactory = $this->getMockBuilder(\Magento\Cms\Api\Data\PageInterfaceFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $pageSearchResultFactory = $this->getMockBuilder(\Magento\Cms\Api\Data\PageSearchResultsInterfaceFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $collectionFactory = $this->getMockBuilder(\Magento\Cms\Model\ResourceModel\Page\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $store->expects($this->any())->method('getId')->willReturn(0); - $this->storeManager->expects($this->any())->method('getStore')->willReturn($store); - - $this->page = $this->getMockBuilder(\Magento\Cms\Model\Page::class)->disableOriginalConstructor()->getMock(); - $this->pageData = $this->getMockBuilder(\Magento\Cms\Api\Data\PageInterface::class) - ->getMock(); - $this->pageSearchResult = $this->getMockBuilder(\Magento\Cms\Api\Data\PageSearchResultsInterface::class) - ->getMock(); - $this->collection = $this->getMockBuilder(\Magento\Cms\Model\ResourceModel\Page\Collection::class) - ->disableOriginalConstructor() - ->setMethods(['getSize', 'setCurPage', 'setPageSize', 'load', 'addOrder']) - ->getMock(); - - $pageFactory->expects($this->any()) - ->method('create') - ->willReturn($this->page); - $pageDataFactory->expects($this->any()) - ->method('create') - ->willReturn($this->pageData); - $pageSearchResultFactory->expects($this->any()) - ->method('create') - ->willReturn($this->pageSearchResult); - $collectionFactory->expects($this->any()) - ->method('create') - ->willReturn($this->collection); - /** - * @var \Magento\Cms\Model\PageFactory $pageFactory - * @var \Magento\Cms\Api\Data\PageInterfaceFactory $pageDataFactory - * @var \Magento\Cms\Api\Data\PageSearchResultsInterfaceFactory $pageSearchResultFactory - * @var \Magento\Cms\Model\ResourceModel\Page\CollectionFactory $collectionFactory - */ - - $this->dataHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) - ->getMockForAbstractClass(); - - $this->repository = new PageRepository( - $this->pageResource, - $pageFactory, - $pageDataFactory, - $collectionFactory, - $pageSearchResultFactory, - $this->dataHelper, - $this->dataObjectProcessor, - $this->storeManager, - $this->collectionProcessor - ); - } - - /** - * @test - */ - public function testSave() - { - $this->pageResource->expects($this->once()) - ->method('save') - ->with($this->page) - ->willReturnSelf(); - $this->assertEquals($this->page, $this->repository->save($this->page)); - } - - /** - * @test - */ - public function testDeleteById() - { - $pageId = '123'; - - $this->page->expects($this->once()) - ->method('getId') - ->willReturn(true); - $this->page->expects($this->once()) - ->method('load') - ->with($pageId) - ->willReturnSelf(); - $this->pageResource->expects($this->once()) - ->method('delete') - ->with($this->page) - ->willReturnSelf(); - - $this->assertTrue($this->repository->deleteById($pageId)); - } - - /** - * @test - * - * @expectedException \Magento\Framework\Exception\CouldNotSaveException - */ - public function testSaveException() - { - $this->pageResource->expects($this->once()) - ->method('save') - ->with($this->page) - ->willThrowException(new \Exception()); - $this->repository->save($this->page); - } - - /** - * @test - * - * @expectedException \Magento\Framework\Exception\CouldNotDeleteException - */ - public function testDeleteException() - { - $this->pageResource->expects($this->once()) - ->method('delete') - ->with($this->page) - ->willThrowException(new \Exception()); - $this->repository->delete($this->page); - } - - /** - * @test - * - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - */ - public function testGetByIdException() - { - $pageId = '123'; - - $this->page->expects($this->once()) - ->method('getId') - ->willReturn(false); - $this->page->expects($this->once()) - ->method('load') - ->with($pageId) - ->willReturnSelf(); - $this->repository->getById($pageId); - } - - /** - * @test - */ - public function testGetList() - { - $total = 10; - - /** @var \Magento\Framework\Api\SearchCriteriaInterface $criteria */ - $criteria = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteriaInterface::class)->getMock(); - - $this->collection->addItem($this->page); - $this->collection->expects($this->once()) - ->method('getSize') - ->willReturn($total); - - $this->collectionProcessor->expects($this->once()) - ->method('process') - ->with($criteria, $this->collection) - ->willReturnSelf(); - - $this->pageSearchResult->expects($this->once()) - ->method('setSearchCriteria') - ->with($criteria) - ->willReturnSelf(); - $this->pageSearchResult->expects($this->once()) - ->method('setTotalCount') - ->with($total) - ->willReturnSelf(); - $this->pageSearchResult->expects($this->once()) - ->method('setItems') - ->with([$this->page]) - ->willReturnSelf(); - $this->assertEquals($this->pageSearchResult, $this->repository->getList($criteria)); - } -} diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index ed87c66b6e1c5..662aee671dd24 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -454,7 +454,7 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); - $storageCollectionInvMock = $storageCollectionMock->expects($this->exactly(sizeof($expectedRemoveKeys))) + $storageCollectionInvMock = $storageCollectionMock->expects($this->exactly(count($expectedRemoveKeys))) ->method('removeItemByKey'); call_user_func_array([$storageCollectionInvMock, 'withConsecutive'], $expectedRemoveKeys); diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php index 8741e37016b64..4ffe4a6ad8774 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php @@ -58,7 +58,10 @@ protected function setUp() $this->blockActions = $objectManager->getObject( BlockActions::class, - ['context' => $context, 'urlBuilder' => $this->urlBuilder] + [ + 'context' => $context, + 'urlBuilder' => $this->urlBuilder + ] ); $objectManager->setBackwardCompatibleProperty($this->blockActions, 'escaper', $this->escaper); @@ -93,6 +96,7 @@ public function testPrepareDataSource() 'edit' => [ 'href' => 'test/url/edit', 'label' => __('Edit'), + '__disableTmpl' => true, ], 'delete' => [ 'href' => 'test/url/delete', @@ -102,6 +106,7 @@ public function testPrepareDataSource() 'message' => __('Are you sure you want to delete a %1 record?', $title), ], 'post' => true, + '__disableTmpl' => true, ], ], ], diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php index 32bbeed0788a3..53d8ee5220768 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php @@ -65,6 +65,7 @@ public function testPrepareItemsByPageId() 'edit' => [ 'href' => 'test/url/edit', 'label' => __('Edit'), + '__disableTmpl' => true, ], 'delete' => [ 'href' => 'test/url/delete', @@ -75,6 +76,7 @@ public function testPrepareItemsByPageId() '__disableTmpl' => true, ], 'post' => true, + '__disableTmpl' => true, ], ], ], @@ -84,7 +86,6 @@ public function testPrepareItemsByPageId() ->method('escapeHtml') ->with($title) ->willReturn($title); - // Configure mocks and object data $urlBuilderMock->expects($this->any()) ->method('getUrl') @@ -106,7 +107,6 @@ public function testPrepareItemsByPageId() ], ] ); - $model->setName($name); $items = $model->prepareDataSource($items); // Run test diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php index 6e9eef47281c0..65940c5d7b4f9 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php @@ -70,6 +70,7 @@ public function prepareDataSource(array $dataSource) ] ), 'label' => __('Edit'), + '__disableTmpl' => true, ], 'delete' => [ 'href' => $this->urlBuilder->getUrl( @@ -84,6 +85,7 @@ public function prepareDataSource(array $dataSource) 'message' => __('Are you sure you want to delete a %1 record?', $title), ], 'post' => true, + '__disableTmpl' => true, ], ]; } diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php index 5cefa212d1655..fa3756abfded4 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -86,7 +86,8 @@ public function prepareDataSource(array $dataSource) if (isset($item['page_id'])) { $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl($this->editUrl, ['page_id' => $item['page_id']]), - 'label' => __('Edit') + 'label' => __('Edit'), + '__disableTmpl' => true, ]; $title = $this->getEscaper()->escapeHtml($item['title']); $item[$name]['delete'] = [ @@ -98,6 +99,7 @@ public function prepareDataSource(array $dataSource) '__disableTmpl' => true, ], 'post' => true, + '__disableTmpl' => true, ]; } if (isset($item['identifier'])) { @@ -107,7 +109,8 @@ public function prepareDataSource(array $dataSource) isset($item['_first_store_id']) ? $item['_first_store_id'] : null, isset($item['store_code']) ? $item['store_code'] : null ), - 'label' => __('View') + 'label' => __('View'), + '__disableTmpl' => true, ]; } } diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index d04587bbdd728..91036d31fdc2b 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -15,8 +15,7 @@ "magento/module-theme": "*", "magento/module-ui": "*", "magento/module-variable": "*", - "magento/module-widget": "*", - "magento/module-authorization": "*" + "magento/module-widget": "*" }, "suggest": { "magento/module-cms-sample-data": "*" diff --git a/app/code/Magento/Cms/etc/db_schema.xml b/app/code/Magento/Cms/etc/db_schema.xml index 1e64c905badd8..9ff3153098482 100644 --- a/app/code/Magento/Cms/etc/db_schema.xml +++ b/app/code/Magento/Cms/etc/db_schema.xml @@ -68,6 +68,8 @@ comment="Page Custom Template"/> <column xsi:type="text" name="custom_layout_update_xml" nullable="true" comment="Page Custom Layout Update Content"/> + <column xsi:type="varchar" name="layout_update_selected" nullable="true" + length="128" comment="Page Custom Layout File"/> <column xsi:type="date" name="custom_theme_from" comment="Page Custom Theme Active From Date"/> <column xsi:type="date" name="custom_theme_to" comment="Page Custom Theme Active To Date"/> <column xsi:type="varchar" name="meta_title" nullable="true" length="255" comment="Page Meta Title"/> diff --git a/app/code/Magento/Cms/etc/db_schema_whitelist.json b/app/code/Magento/Cms/etc/db_schema_whitelist.json index 8da44baf71719..fe0e9c1f2e22e 100644 --- a/app/code/Magento/Cms/etc/db_schema_whitelist.json +++ b/app/code/Magento/Cms/etc/db_schema_whitelist.json @@ -50,7 +50,8 @@ "custom_layout_update_xml": true, "custom_theme_from": true, "custom_theme_to": true, - "meta_title": true + "meta_title": true, + "layout_update_selected": true }, "index": { "CMS_PAGE_IDENTIFIER": true, diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index e41f500915916..35fb7d82651f0 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -7,15 +7,15 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Cms\Api\Data\PageSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Cms\Model\PageSearchResults" /> <preference for="Magento\Cms\Api\Data\BlockSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Cms\Model\BlockSearchResults" /> <preference for="Magento\Cms\Api\GetBlockByIdentifierInterface" type="Magento\Cms\Model\GetBlockByIdentifier" /> <preference for="Magento\Cms\Api\GetPageByIdentifierInterface" type="Magento\Cms\Model\GetPageByIdentifier" /> <preference for="Magento\Cms\Api\Data\PageInterface" type="Magento\Cms\Model\Page" /> <preference for="Magento\Cms\Api\Data\BlockInterface" type="Magento\Cms\Model\Block" /> <preference for="Magento\Cms\Api\BlockRepositoryInterface" type="Magento\Cms\Model\BlockRepository" /> - <preference for="Magento\Cms\Api\PageRepositoryInterface" type="Magento\Cms\Model\PageRepository" /> + <preference for="Magento\Cms\Api\PageRepositoryInterface" type="Magento\Cms\Model\PageRepository\ValidationComposite" /> <preference for="Magento\Ui\Component\Wysiwyg\ConfigInterface" type="Magento\Cms\Model\Wysiwyg\Config"/> <preference for="Magento\Cms\Api\GetUtilityPageIdentifiersInterface" type="Magento\Cms\Model\GetUtilityPageIdentifiers" /> <type name="Magento\Cms\Model\Wysiwyg\Config"> @@ -235,4 +235,19 @@ <type name="Magento\Catalog\Model\Product"> <plugin name="cms" type="Magento\Cms\Model\Plugin\Product" sortOrder="100"/> </type> + <type name="Magento\Cms\Model\PageRepository\ValidationComposite"> + <arguments> + <argument name="repository" xsi:type="object">Magento\Cms\Model\PageRepository</argument> + <argument name="validators" xsi:type="array"> + <item name="layout_update" xsi:type="object">Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator</item> + </argument> + </arguments> + </type> + <preference for="Magento\Cms\Model\Page\CustomLayoutManagerInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutManager" /> + <type name="Magento\Cms\Model\Page\CustomLayout\CustomLayoutManager"> + <arguments> + <argument name="themeFactory" xsi:type="object">Magento\Framework\View\Design\Theme\FlyweightFactory\Proxy</argument> + </arguments> + </type> + <preference for="Magento\Cms\Model\Page\CustomLayoutRepositoryInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutRepository" /> </config> diff --git a/app/code/Magento/Cms/etc/events.xml b/app/code/Magento/Cms/etc/events.xml index d6b9ad4ee0248..1ad847e215ccc 100644 --- a/app/code/Magento/Cms/etc/events.xml +++ b/app/code/Magento/Cms/etc/events.xml @@ -36,4 +36,7 @@ <event name="magento_cms_api_data_pageinterface_load_after"> <observer name="legacy_model_cms_page_after_load" instance="Magento\Framework\EntityManager\Observer\AfterEntityLoad" /> </event> + <event name="cms_page_prepare_save"> + <observer name="validate_cms_page" instance="Magento\Cms\Observer\PageValidatorObserver" /> + </event> </config> diff --git a/app/code/Magento/Cms/etc/webapi_rest/di.xml b/app/code/Magento/Cms/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..686305f2ed300 --- /dev/null +++ b/app/code/Magento/Cms/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ +<?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\Cms\Api\PageRepositoryInterface"> + <plugin name="aclCheck" type="Magento\Cms\Observer\PageAclPlugin" sortOrder="100" /> + </type> +</config> diff --git a/app/code/Magento/Cms/etc/webapi_soap/di.xml b/app/code/Magento/Cms/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..686305f2ed300 --- /dev/null +++ b/app/code/Magento/Cms/etc/webapi_soap/di.xml @@ -0,0 +1,12 @@ +<?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\Cms\Api\PageRepositoryInterface"> + <plugin name="aclCheck" type="Magento\Cms\Observer\PageAclPlugin" sortOrder="100" /> + </type> +</config> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml index ad0f33df59d4e..a2ce0ec1b8740 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_form.xml @@ -105,6 +105,7 @@ <settings> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> + <rule name="no-marginal-whitespace" xsi:type="boolean">true</rule> </validation> <dataType>text</dataType> <label translate="true">Identifier</label> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml index dd58d17cbf577..396923a2b6f3b 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml @@ -249,6 +249,7 @@ </field> <field name="layout_update_xml" formElement="textarea"> <argument name="data" xsi:type="array"> + <item name="disabled" xsi:type="boolean">true</item> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">page</item> </item> @@ -259,6 +260,26 @@ <dataScope>layout_update_xml</dataScope> </settings> </field> + <field name="custom_layout_update_select" formElement="select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="source" xsi:type="string">page</item> + </item> + </argument> + <settings> + <dataType>text</dataType> + <label translate="true">Custom Layout Update</label> + <tooltip> + <description translate="true"> + List of layout files with an update handle "selectable" + matching *PageIdentifier*_*UpdateID*. + <br/> + See Magento documentation for more information + </description> + </tooltip> + <dataScope>layout_update_selected</dataScope> + </settings> + </field> </fieldset> <fieldset name="custom_design_update" sortOrder="60"> <settings> diff --git a/app/code/Magento/Cms/view/adminhtml/web/css/source/_module.less b/app/code/Magento/Cms/view/adminhtml/web/css/source/_module.less deleted file mode 100644 index af8fa2758f5af..0000000000000 --- a/app/code/Magento/Cms/view/adminhtml/web/css/source/_module.less +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -.modal-slide { - .media-gallery-modal { - .page-main-actions { - margin-bottom: 3rem; - } - - #new_folder { - margin-right: 10px; - } - } -} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php index fa4944381b858..21bdca732b606 100644 --- a/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php @@ -56,7 +56,7 @@ public function getData(string $blockIdentifier): array ); } - $renderedContent = $this->widgetFilter->filter($block->getContent()); + $renderedContent = $this->widgetFilter->filterDirective($block->getContent()); $blockData = [ BlockInterface::BLOCK_ID => $block->getId(), diff --git a/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/CmsPageUrlPathGeneratorTest.php b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/CmsPageUrlPathGeneratorTest.php new file mode 100644 index 0000000000000..6b57254dd0ec1 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/CmsPageUrlPathGeneratorTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Test\Unit\Model; + +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Filter\FilterManager; +use Magento\Cms\Api\Data\PageInterface; + +/** + * Class \Magento\CmsUrlRewrite\Test\Unit\Model\CmsPageUrlPathGeneratorTest + */ +class CmsPageUrlPathGeneratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|FilterManager + */ + private $filterManagerMock; + + /** + * @var CmsPageUrlPathGenerator + */ + private $model; + + /** + * Setup environment for test + */ + protected function setUp() + { + $this->objectManager = new ObjectManagerHelper($this); + $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) + ->disableOriginalConstructor() + ->setMethods(['translitUrl']) + ->getMock(); + + $this->model = $this->objectManager->getObject( + CmsPageUrlPathGenerator::class, + [ + 'filterManager' => $this->filterManagerMock + ] + ); + } + + /** + * Test getUrlPath with page has identifier = cms-cookie + */ + public function testGetUrlPath() + { + /* @var PageInterface $cmsPageMock*/ + $cmsPageMock = $this->getMockBuilder(PageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $cmsPageMock->expects($this->any()) + ->method('getIdentifier') + ->willReturn('cms-cookie'); + + $this->assertEquals('cms-cookie', $this->model->getUrlPath($cmsPageMock)); + } + + /** + * Test getCanonicalUrlPath() with page has id = 1 + */ + public function testGetCanonicalUrlPath() + { + /* @var PageInterface $cmsPageMock*/ + $cmsPageMock = $this->getMockBuilder(PageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $cmsPageMock->expects($this->any()) + ->method('getId') + ->willReturn('1'); + + $this->assertEquals('cms/page/view/page_id/1', $this->model->getCanonicalUrlPath($cmsPageMock)); + } + + /** + * Test generateUrlKey() with page has no identifier + */ + public function testGenerateUrlKeyWithNullIdentifier() + { + /** + * Data set + */ + $page = [ + 'identifier' => null, + 'title' => 'CMS Cookie' + ]; + + /* @var PageInterface $cmsPageMock*/ + $cmsPageMock = $this->getMockBuilder(PageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $cmsPageMock->expects($this->any()) + ->method('getIdentifier') + ->willReturn($page['identifier']); + + $cmsPageMock->expects($this->any()) + ->method('getTitle') + ->willReturn($page['title']); + + $this->filterManagerMock->expects($this->any()) + ->method('translitUrl') + ->with($page['title']) + ->willReturn('cms-cookie'); + + $this->assertEquals('cms-cookie', $this->model->generateUrlKey($cmsPageMock)); + } + + /** + * Test generateUrlKey() with page has identifier + */ + public function testGenerateUrlKeyWithIdentifier() + { + /** + * Data set + */ + $page = [ + 'identifier' => 'home', + 'title' => 'Home Page' + ]; + + /* @var PageInterface $cmsPageMock*/ + $cmsPageMock = $this->getMockBuilder(PageInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $cmsPageMock->expects($this->any()) + ->method('getIdentifier') + ->willReturn($page['identifier']); + + $cmsPageMock->expects($this->any()) + ->method('getTitle') + ->willReturn($page['title']); + + $this->filterManagerMock->expects($this->any()) + ->method('translitUrl') + ->with($page['identifier']) + ->willReturn('home'); + + $this->assertEquals('home', $this->model->generateUrlKey($cmsPageMock)); + } +} diff --git a/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php b/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php new file mode 100644 index 0000000000000..8716fe5a23ad3 --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Config\Model\Config\Backend\File; + +/** + * System config PDF field backend model. + */ +class Pdf extends \Magento\Config\Model\Config\Backend\File +{ + /** + * @inheritdoc + */ + protected function _getAllowedExtensions() + { + return ['pdf']; + } +} diff --git a/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php b/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php index 44131fe8a7966..a5a81a4dde75d 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php +++ b/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php @@ -4,24 +4,24 @@ * See COPYING.txt for license details. */ -/** - * System config image field backend model for Zend PDF generator - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Config\Model\Config\Backend\Image; /** + * System config PDF field backend model. + * * @api * @since 100.0.2 + * @see \Magento\Config\Model\Config\Backend\File\Pdf */ class Pdf extends \Magento\Config\Model\Config\Backend\Image { /** + * Returns the list of allowed file extensions. + * * @return string[] */ protected function _getAllowedExtensions() { - return ['tif', 'tiff', 'png', 'jpg', 'jpe', 'jpeg']; + return ['tif', 'tiff', 'png', 'jpg', 'jpe', 'jpeg', 'pdf']; } } diff --git a/app/code/Magento/Config/Model/Config/Source/Email/Template.php b/app/code/Magento/Config/Model/Config/Source/Email/Template.php index 04222733418d3..e4f1ae65bcacd 100644 --- a/app/code/Magento/Config/Model/Config/Source/Email/Template.php +++ b/app/code/Magento/Config/Model/Config/Source/Email/Template.php @@ -6,6 +6,8 @@ namespace Magento\Config\Model\Config\Source\Email; /** + * Source for template + * * @api * @since 100.0.2 */ @@ -62,6 +64,12 @@ public function toOptionArray() $templateLabel = $this->_emailConfig->getTemplateLabel($templateId); $templateLabel = __('%1 (Default)', $templateLabel); array_unshift($options, ['value' => $templateId, 'label' => $templateLabel]); + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); return $options; } } diff --git a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php index db815ec87ed76..23a3dea1a7029 100644 --- a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php +++ b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php @@ -40,7 +40,7 @@ abstract class AbstractElement implements StructureElementInterface protected $_storeManager; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -50,15 +50,11 @@ abstract class AbstractElement implements StructureElementInterface private $elementVisibility; /** - * Construct. - * * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager */ - public function __construct( - StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager - ) { + public function __construct(StoreManagerInterface $storeManager, \Magento\Framework\Module\Manager $moduleManager) + { $this->_storeManager = $storeManager; $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php b/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php index efb918226aa31..a14114a116e78 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php +++ b/app/code/Magento/Config/Model/Config/Structure/Element/AbstractComposite.php @@ -23,12 +23,12 @@ abstract class AbstractComposite extends \Magento\Config\Model\Config\Structure\ /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param Iterator $childrenIterator */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, Iterator $childrenIterator ) { parent::__construct($storeManager, $moduleManager); diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Dependency/Field.php b/app/code/Magento/Config/Model/Config/Structure/Element/Dependency/Field.php index 8f4d82eed51c5..15f23c7737294 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Element/Dependency/Field.php +++ b/app/code/Magento/Config/Model/Config/Structure/Element/Dependency/Field.php @@ -9,6 +9,12 @@ * @api * @since 100.0.2 */ + +/** + * Class Field + * + * Fields are used to describe possible values for a type/interface. + */ class Field { /** @@ -41,7 +47,7 @@ public function __construct(array $fieldData = [], $fieldPrefix = "") if (isset($fieldData['separator'])) { $this->_values = explode($fieldData['separator'], $fieldData['value']); } else { - $this->_values = [$fieldData['value']]; + $this->_values = [isset($fieldData['value']) ? $fieldData['value'] : '']; } $fieldId = $fieldPrefix . (isset( $fieldData['dependPath'] diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Field.php b/app/code/Magento/Config/Model/Config/Structure/Element/Field.php index 6a8cc6e767466..834b2a9e17e37 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Element/Field.php +++ b/app/code/Magento/Config/Model/Config/Structure/Element/Field.php @@ -56,7 +56,7 @@ class Field extends \Magento\Config\Model\Config\Structure\AbstractElement /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Config\Model\Config\BackendFactory $backendFactory * @param \Magento\Config\Model\Config\SourceFactory $sourceFactory * @param \Magento\Config\Model\Config\CommentFactory $commentFactory @@ -65,7 +65,7 @@ class Field extends \Magento\Config\Model\Config\Structure\AbstractElement */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Config\Model\Config\BackendFactory $backendFactory, \Magento\Config\Model\Config\SourceFactory $sourceFactory, \Magento\Config\Model\Config\CommentFactory $commentFactory, diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Group.php b/app/code/Magento/Config/Model/Config/Structure/Element/Group.php index db479e8b795a0..1ca0afa942f8d 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Element/Group.php +++ b/app/code/Magento/Config/Model/Config/Structure/Element/Group.php @@ -29,14 +29,14 @@ class Group extends AbstractComposite /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param Iterator\Field $childrenIterator * @param \Magento\Config\Model\Config\BackendClone\Factory $cloneModelFactory * @param Dependency\Mapper $dependencyMapper */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Config\Model\Config\Structure\Element\Iterator\Field $childrenIterator, \Magento\Config\Model\Config\BackendClone\Factory $cloneModelFactory, \Magento\Config\Model\Config\Structure\Element\Dependency\Mapper $dependencyMapper diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Section.php b/app/code/Magento/Config/Model/Config/Structure/Element/Section.php index 134411fbd87ca..80c029dfea2d0 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Element/Section.php +++ b/app/code/Magento/Config/Model/Config/Structure/Element/Section.php @@ -22,13 +22,13 @@ class Section extends AbstractComposite /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param Iterator $childrenIterator * @param \Magento\Framework\AuthorizationInterface $authorization */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, Iterator $childrenIterator, \Magento\Framework\AuthorizationInterface $authorization ) { diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml index 10d22b61ecae0..2a3a14293a059 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SetGroupForValidVATIdIntraUnionActionGroup"> <annotations> - <description>Goes to the 'Configuration' page for 'Customer Configuration'. Sets the 'Group For Valid VAT ID Intra Union' option. Clicks on the Save button. Validates the the Save message is present.</description> + <description>Goes to the 'Configuration' page for 'Customer Configuration'. Sets the 'Group For Valid VAT ID Intra Union' option. Clicks on the Save button. Validates the Save message is present.</description> </annotations> <arguments> <argument name="value" type="string"/> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml new file mode 100644 index 0000000000000..79505e0627865 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml @@ -0,0 +1,23 @@ +<?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="AdminExpandConfigTabActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page and expands main level configuration tab passed via argument as Tab Name.</description> + </annotations> + <arguments> + <argument name="tabName" type="string"/> + </arguments> + + <scrollTo stepKey="scrollToTab" selector="{{AdminConfigSection.collapsibleTabByTitle(tabName)}}" x="0" y="-80"/> + <conditionalClick selector="{{AdminConfigSection.collapsibleTabByTitle(tabName)}}" dependentSelector="{{AdminConfigSection.expandedTabByTitle(tabName)}}" visible="false" stepKey="expandTab" /> + <waitForElement selector="{{AdminConfigSection.expandedTabByTitle(tabName)}}" stepKey="waitOpenedTab" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandSecurityTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandSecurityTabActionGroup.xml new file mode 100644 index 0000000000000..f07d4df9d86b2 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandSecurityTabActionGroup.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="AdminExpandSecurityTabActionGroup"> + <conditionalClick selector="{{AdminSection.SecurityTab}}" dependentSelector="{{AdminSection.CheckIfTabExpand}}" visible="true" stepKey="openSecurityTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigAdminPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigAdminPageActionGroup.xml new file mode 100644 index 0000000000000..6d4fba179ecf4 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigAdminPageActionGroup.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="AdminOpenConfigAdminPageActionGroup"> + <amOnPage url="{{AdminConfigAdminPage.url}}" stepKey="goToConfigAdminSectionPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml new file mode 100644 index 0000000000000..eaca27f86f49a --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminOpenConfigNavItemActionGroup"> + <annotations> + <description>Clicks on config nav item selected by passed argument.</description> + </annotations> + <arguments> + <argument name="navItem" type="string"/> + </arguments> + + <scrollTo stepKey="scrollToNavItem" selector="{{AdminConfigSection.navItemByTitle(navItem)}}" x="0" y="-80"/> + <click selector="{{AdminConfigSection.navItemByTitle(navItem)}}" stepKey="openNavItem" /> + <waitForElement selector="{{AdminConfigSection.activeNavItemByTitle(navItem)}}" stepKey="waitActiveNavItem" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml new file mode 100644 index 0000000000000..4c5d21a890973 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AdminOpenStoreConfigDeveloperPageActionGroup"> + <annotations> + <description>Go to admin store configuration developer page.</description> + </annotations> + + <amOnPage url="{{AdminConfigDeveloperPage.url}}" stepKey="openAdminStoreConfigDeveloperPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml new file mode 100644 index 0000000000000..43343fd0851e4 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AdminOpenStoreConfigPageActionGroup"> + <annotations> + <description>Go to admin store configuration page.</description> + </annotations> + + <amOnPage url="{{AdminConfigPage.url}}" stepKey="openAdminStoreConfigPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml index 6ed0cfe95cb94..bd23292d3ee6a 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminSaveConfigActionGroup"> <click selector="{{AdminConfigSection.saveButton}}" stepKey="clickSaveConfigBtn"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSetMaximumLoginFailuresToLockoutAccountActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSetMaximumLoginFailuresToLockoutAccountActionGroup.xml new file mode 100644 index 0000000000000..ada58e9f4225e --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSetMaximumLoginFailuresToLockoutAccountActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AdminSetMaximumLoginFailuresToLockoutAccountActionGroup"> + <arguments> + <argument name="qty" type="string" defaultValue="3"/> + </arguments> + <uncheckOption selector="{{AdminSection.systemValueForMaximumLoginFailures}}" stepKey="uncheckUseSystemValue"/> + <fillField selector="{{AdminSection.MaximumLoginFailures}}" userInput="{{qty}}" stepKey="setMaximumLoginFailures"/> + <seeInField selector="{{AdminSection.MaximumLoginFailures}}" userInput="{{qty}}" stepKey="seeNewValueInField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml index 45d84a338a30b..d65376828e2c4 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml @@ -27,7 +27,6 @@ <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToConfigGeneralPage"/> <waitForPageLoad stepKey="waitForConfigPageLoad"/> </actionGroup> - <actionGroup name="SelectTopDestinationsCountry"> <annotations> <description>Selects the provided Countries under 'Top destinations' on the 'General' section of the 'Configuration' page. Clicks on the Save button.</description> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigAdminPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigAdminPage.xml new file mode 100644 index 0000000000000..661bb734bcbe4 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigAdminPage.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="AdminConfigAdminPage" url="admin/system_config/edit/section/admin" module="Magento_Config" area="admin"> + <section name="AdminSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml index 7a62dfff8323b..a1ee5348d094c 100644 --- a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml @@ -21,4 +21,10 @@ <page name="AdminConfigGeneralPage" url="admin/system_config/edit/section/general/" area="admin" module="Magento_Config"> <section name="GeneralSection"/> </page> + <page name="AdminConfigDeveloperPage" url="admin/system_config/edit/section/dev/" area="admin" module="Magento_Config"> + <section name="AdminConfigSection" /> + </page> + <page name="AdminConfigAdvancedAdmin" url="admin/system_config/edit/section/admin/" area="admin" module="Magento_Config"> + <section name="AdvanceAdminSection"/> + </page> </pages> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml index fd49c1482c133..ffe3f0076ca8d 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml @@ -7,6 +7,11 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminConfigSection"> + <element name="collapsibleTabByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@data-role='title'][contains(.,'{{tabTitle}}')]" parameterized="true" /> + <element name="expandedTabByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@data-role='title'][@aria-expanded='true'][contains(.,'{{tabTitle}}')]" parameterized="true" /> + <element name="notExpandedTabByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@data-role='title'][@aria-expanded='false'][contains(.,'{{tabTitle}}')]" parameterized="true" /> + <element name="navItemByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@role='tablist']//li[contains(@class, 'nav-item')][contains(.,'{{navItem}}')]" parameterized="true" /> + <element name="activeNavItemByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@role='tablist']//li[contains(@class, 'nav-item')][contains(@class, '_active')][contains(.,'{{navItem}}')]" parameterized="true" /> <element name="saveButton" type="button" selector="#save"/> <element name="generalTab" type="text" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='General']"/> <element name="generalTabClosed" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='false' or @aria-expanded='0']//strong[text()='General']"/> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml index 7b6c9f8ab3b79..4aea038bec716 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSection.xml @@ -13,5 +13,7 @@ <element name="SecurityTab" type="button" selector="#admin_security-head"/> <element name="AdminAccountSharing" type="button" selector="#admin_security_admin_account_sharing"/> <element name="EnableSystemValue" type="button" selector="#admin_security_admin_account_sharing_inherit"/> + <element name="systemValueForMaximumLoginFailures" type="checkbox" selector="#admin_security_lockout_failures_inherit"/> + <element name="MaximumLoginFailures" type="input" selector="#admin_security_lockout_failures"/> </section> </sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml similarity index 55% rename from app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml rename to app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml index 621f2e6a67688..762d17bdf87f1 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml +++ b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml @@ -6,11 +6,14 @@ */ --> <suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> - <suite name="PaypalTestSuite"> + <suite name="AppConfigDumpSuite"> + <before> + <magentoCLI command="app:config:dump" stepKey="configDump"/> + </before> + <after> + </after> <include> - <test name="CheckDefaultValueOfPayPalCustomizeButtonTest"/> - <test name="PayPalSmartButtonInCheckoutPage"/> - <test name="CheckCreditButtonConfiguration"/> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"/> </include> </suite> -</suites> \ No newline at end of file +</suites> diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php index b29ad244e7a81..9fabe6fef0c8e 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php @@ -6,6 +6,9 @@ namespace Magento\Config\Test\Unit\Model\Config\Source\Email; +/** + * Test class for Template. + */ class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -76,9 +79,21 @@ public function testToOptionArray() $this->returnValue('Template New') ); $expectedResult = [ - ['value' => 'template_new', 'label' => 'Template New (Default)'], - ['value' => 'template_one', 'label' => 'Template One'], - ['value' => 'template_two', 'label' => 'Template Two'], + [ + 'value' => 'template_new', + 'label' => 'Template New (Default)', + '__disableTmpl' => true + ], + [ + 'value' => 'template_one', + 'label' => 'Template One', + '__disableTmpl' => true + ], + [ + 'value' => 'template_two', + 'label' => 'Template Two', + '__disableTmpl' => true + ], ]; $this->_model->setPath('template/new'); $this->assertEquals($expectedResult, $this->_model->toOptionArray()); diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php index e448b628ef020..8c54a02a491c0 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/AbstractCompositeTest.php @@ -29,7 +29,7 @@ class AbstractCompositeTest extends \PHPUnit\Framework\TestCase protected $_iteratorMock; /** - * @var \Magento\Framework\Module\ModuleManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager | \PHPUnit_Framework_MockObject_MockObject */ protected $moduleManagerMock; @@ -56,7 +56,7 @@ protected function setUp() ->getMockForAbstractClass(); $this->_iteratorMock = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Iterator::class); $this->_storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManager::class); - $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); + $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); $this->_model = $this->getMockForAbstractClass( \Magento\Config\Model\Config\Structure\Element\AbstractComposite::class, [$this->_storeManagerMock, $this->moduleManagerMock, $this->_iteratorMock] diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php index 750a829eef7ec..22cf979a9bd63 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php @@ -12,6 +12,8 @@ class FieldTest extends \PHPUnit\Framework\TestCase */ const SIMPLE_VALUE = 'someValue'; + const EMPTY_VALUE = ''; + const COMPLEX_VALUE1 = 'value_1'; const COMPLEX_VALUE2 = 'value_2'; @@ -150,8 +152,19 @@ public function getValuesDataProvider() return [ [$this->_getSimpleData(), true, [self::SIMPLE_VALUE]], [$this->_getSimpleData(), false, [self::SIMPLE_VALUE]], + [$this->_getSimpleEmptyData(), false, [static::EMPTY_VALUE]], [$this->_getComplexData(), true, $complexDataValues], [$this->_getComplexData(), false, $complexDataValues] ]; } + + /** + * Providing a field data with no field value + * + * @return array + */ + protected function _getSimpleEmptyData(): array + { + return ['dependPath' => ['section_2', 'group_3', 'field_4']]; + } } diff --git a/app/code/Magento/Config/etc/db_schema.xml b/app/code/Magento/Config/etc/db_schema.xml index 8aeac802fbd91..54680c0be7b06 100644 --- a/app/code/Magento/Config/etc/db_schema.xml +++ b/app/code/Magento/Config/etc/db_schema.xml @@ -9,12 +9,14 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="core_config_data" resource="default" engine="innodb" comment="Config Data"> <column xsi:type="int" name="config_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Config Id"/> + comment="Config ID"/> <column xsi:type="varchar" name="scope" nullable="false" length="8" default="default" comment="Config Scope"/> <column xsi:type="int" name="scope_id" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Config Scope Id"/> + default="0" comment="Config Scope ID"/> <column xsi:type="varchar" name="path" nullable="false" length="255" default="general" comment="Config Path"/> <column xsi:type="text" name="value" nullable="true" comment="Config Value"/> + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" + comment="Updated At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="config_id"/> </constraint> diff --git a/app/code/Magento/Config/etc/db_schema_whitelist.json b/app/code/Magento/Config/etc/db_schema_whitelist.json index 850e160bc732f..51ca41d8e6af8 100644 --- a/app/code/Magento/Config/etc/db_schema_whitelist.json +++ b/app/code/Magento/Config/etc/db_schema_whitelist.json @@ -5,11 +5,12 @@ "scope": true, "scope_id": true, "path": true, - "value": true + "value": true, + "updated_at": true }, "constraint": { "PRIMARY": true, "CORE_CONFIG_DATA_SCOPE_SCOPE_ID_PATH": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Config/i18n/en_US.csv b/app/code/Magento/Config/i18n/en_US.csv index 9770bf4b94c27..ceb1efdc8b77d 100644 --- a/app/code/Magento/Config/i18n/en_US.csv +++ b/app/code/Magento/Config/i18n/en_US.csv @@ -118,3 +118,4 @@ Dashboard,Dashboard "Web Section","Web Section" "Store Email Addresses Section","Store Email Addresses Section" "Email to a Friend","Email to a Friend" +"Taiwan","Taiwan, Province of China" diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php index 4874dc8ea03ae..b9fcf307b613f 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php @@ -197,6 +197,7 @@ protected function getAttributes() foreach ($attributes as $key => $attribute) { if (isset($configurableData[$key])) { $attributes[$key] = array_replace_recursive($attribute, $configurableData[$key]); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $attributes[$key]['values'] = array_merge( isset($attribute['values']) ? $attribute['values'] : [], isset($configurableData[$key]['values']) @@ -411,15 +412,17 @@ private function prepareAttributes( 'id' => $attribute->getAttributeId(), 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], 'chosen' => [], + '__disableTmpl' => true ]; - foreach ($attribute->getOptions() as $option) { - if (!empty($option->getValue())) { + $options = $attribute->usesSource() ? $attribute->getSource()->getAllOptions() : []; + foreach ($options as $option) { + if (!empty($option['value'])) { $attributes[$attribute->getAttributeId()]['options'][] = [ 'attribute_code' => $attribute->getAttributeCode(), 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $option->getValue(), - 'label' => $option->getLabel(), - 'value' => $option->getValue(), + 'id' => $option['value'], + 'label' => $option['label'], + 'value' => $option['value'], '__disableTmpl' => true, ]; } diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index 71db9d32aa593..55c0c8f6ca4ce 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -182,11 +182,10 @@ public function getAllowProducts() { if (!$this->hasAllowProducts()) { $products = []; - $skipSaleableCheck = $this->catalogProduct->getSkipSaleableCheck(); $allProducts = $this->getProduct()->getTypeInstance()->getUsedProducts($this->getProduct(), null); /** @var $product \Magento\Catalog\Model\Product */ foreach ($allProducts as $product) { - if ($skipSaleableCheck || ((int) $product->getStatus()) === Status::STATUS_ENABLED) { + if ((int) $product->getStatus() === Status::STATUS_ENABLED) { $products[] = $product; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php new file mode 100644 index 0000000000000..19a1b8d3ca17f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php @@ -0,0 +1,190 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Customer\Model\Session; +use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * Cache of used products for configurable product + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class UsedProductsCache +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var FrontendInterface + */ + private $cache; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var ProductInterfaceFactory + */ + private $productFactory; + + /** + * @var Session + */ + private $customerSession; + + /** + * @param MetadataPool $metadataPool + * @param FrontendInterface $cache + * @param SerializerInterface $serializer + * @param ProductInterfaceFactory $productFactory + * @param Session $customerSession + */ + public function __construct( + MetadataPool $metadataPool, + FrontendInterface $cache, + SerializerInterface $serializer, + ProductInterfaceFactory $productFactory, + Session $customerSession + ) { + $this->metadataPool = $metadataPool; + $this->cache = $cache; + $this->serializer = $serializer; + $this->productFactory = $productFactory; + $this->customerSession = $customerSession; + } + + /** + * Retrieve used products for configurable product + * + * @param Configurable $subject + * @param callable $proceed + * @param Product $product + * @param array|null $requiredAttributeIds + * @return ProductInterface[] + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundGetUsedProducts( + Configurable $subject, + callable $proceed, + $product, + $requiredAttributeIds = null + ) { + $cacheKey = $this->getCacheKey($product, $requiredAttributeIds); + $usedProducts = $this->readUsedProductsCacheData($cacheKey); + if ($usedProducts === null) { + $usedProducts = $proceed($product, $requiredAttributeIds); + $this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey); + } + + return $usedProducts; + } + + /** + * Generate cache key for product + * + * @param Product $product + * @param array|null $requiredAttributeIds + * @return string + */ + private function getCacheKey($product, $requiredAttributeIds = null): string + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $keyParts = [ + 'getUsedProducts', + $product->getData($metadata->getLinkField()), + $product->getStoreId(), + $this->customerSession->getCustomerGroupId(), + ]; + if ($requiredAttributeIds !== null) { + sort($requiredAttributeIds); + $keyParts[] = implode('', $requiredAttributeIds); + } + $cacheKey = sha1(implode('_', $keyParts)); + + return $cacheKey; + } + + /** + * Read used products data from cache + * + * Looking for cache record stored under provided $cacheKey + * In case data exists turns it into array of products + * + * @param string $cacheKey + * @return ProductInterface[]|null + */ + private function readUsedProductsCacheData(string $cacheKey): ?array + { + $data = $this->cache->load($cacheKey); + if (!$data) { + return null; + } + + $items = $this->serializer->unserialize($data); + if (!$items) { + return null; + } + + $usedProducts = []; + foreach ($items as $item) { + /** @var Product $productItem */ + $productItem = $this->productFactory->create(); + $productItem->setData($item); + $usedProducts[] = $productItem; + } + + return $usedProducts; + } + + /** + * Save $subProducts to cache record identified with provided $cacheKey + * + * Cached data will be tagged with combined list of product tags and data specific tags i.e. 'price' etc. + * + * @param Product $product + * @param ProductInterface[] $subProducts + * @param string $cacheKey + * @return bool + */ + private function saveUsedProductsCacheData(Product $product, array $subProducts, string $cacheKey): bool + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $data = $this->serializer->serialize( + array_map( + function ($item) { + return $item->getData(); + }, + $subProducts + ) + ); + $tags = array_merge( + $product->getIdentities(), + [ + Category::CACHE_TAG, + Product::CACHE_TAG, + 'price', + Configurable::TYPE_CODE . '_' . $product->getData($metadata->getLinkField()), + ] + ); + $result = $this->cache->save($data, $cacheKey, $tags); + + return (bool) $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index a849d964eaed5..5b50cc0ebd5e0 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -9,12 +9,14 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Api\SearchCriteriaBuilder; /** * Configurable product type implementation @@ -194,9 +196,18 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $salableProcessor; + /** + * @var ProductAttributeRepositoryInterface|null + */ + private $productAttributeRepository; + + /** + * @var SearchCriteriaBuilder|null + */ + private $searchCriteriaBuilder; + /** * @codingStandardsIgnoreStart/End - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -214,9 +225,13 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable $catalogProductTypeConfigurable * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor + * @param \Magento\Framework\Cache\FrontendInterface|null $cache + * @param \Magento\Customer\Model\Session|null $customerSession * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param ProductInterfaceFactory $productFactory * @param SalableProcessor $salableProcessor + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -241,7 +256,9 @@ public function __construct( \Magento\Customer\Model\Session $customerSession = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, ProductInterfaceFactory $productFactory = null, - SalableProcessor $salableProcessor = null + SalableProcessor $salableProcessor = null, + ProductAttributeRepositoryInterface $productAttributeRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -256,6 +273,10 @@ public function __construct( $this->productFactory = $productFactory ?: ObjectManager::getInstance() ->get(ProductInterfaceFactory::class); $this->salableProcessor = $salableProcessor ?: ObjectManager::getInstance()->get(SalableProcessor::class); + $this->productAttributeRepository = $productAttributeRepository ?: + ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); parent::__construct( $catalogProductOption, $eavConfig, @@ -1231,30 +1252,21 @@ public function isPossibleBuyFromList($product) /** * Returns array of sub-products for specified configurable product - * - * $requiredAttributeIds - one dimensional array, if provided - * * Result array contains all children for specified configurable product * - * @param \Magento\Catalog\Model\Product $product - * @param array $requiredAttributeIds + * @param \Magento\Catalog\Model\Product $product + * @param array $requiredAttributeIds Attributes to include in the select; one-dimensional array * @return ProductInterface[] */ public function getUsedProducts($product, $requiredAttributeIds = null) { - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); - $keyParts = [ - __METHOD__, - $product->getData($metadata->getLinkField()), - $product->getStoreId(), - $this->getCustomerSession()->getCustomerGroupId() - ]; - if ($requiredAttributeIds !== null) { - sort($requiredAttributeIds); - $keyParts[] = implode('', $requiredAttributeIds); + if (!$product->hasData($this->_usedProducts)) { + $collection = $this->getConfiguredUsedProductCollection($product, false, $requiredAttributeIds); + $usedProducts = array_values($collection->getItems()); + $product->setData($this->_usedProducts, $usedProducts); } - $cacheKey = $this->getUsedProductsCacheKey($keyParts); - return $this->loadUsedProducts($product, $cacheKey); + + return $product->getData($this->_usedProducts); } /** @@ -1396,16 +1408,18 @@ private function getUsedProductsCacheKey($keyParts) /** * Prepare collection for retrieving sub-products of specified configurable product - * * Retrieve related products collection with additional configuration * * @param \Magento\Catalog\Model\Product $product * @param bool $skipStockFilter + * @param array $requiredAttributeIds Attributes to include in the select * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection + * @throws \Magento\Framework\Exception\LocalizedException */ private function getConfiguredUsedProductCollection( \Magento\Catalog\Model\Product $product, - $skipStockFilter = true + $skipStockFilter = true, + $requiredAttributeIds = null ) { $collection = $this->getUsedProductCollection($product); @@ -1413,8 +1427,19 @@ private function getConfiguredUsedProductCollection( $collection->setFlag('has_stock_status_filter', true); } + $attributesForSelect = $this->getAttributesForCollection($product); + if ($requiredAttributeIds) { + $this->searchCriteriaBuilder->addFilter('attribute_id', $requiredAttributeIds, 'in'); + $requiredAttributes = $this->productAttributeRepository + ->getList($this->searchCriteriaBuilder->create())->getItems(); + $requiredAttributeCodes = []; + foreach ($requiredAttributes as $requiredAttribute) { + $requiredAttributeCodes[] = $requiredAttribute->getAttributeCode(); + } + $attributesForSelect = array_unique(array_merge($attributesForSelect, $requiredAttributeCodes)); + } $collection - ->addAttributeToSelect($this->getAttributesForCollection($product)) + ->addAttributeToSelect($attributesForSelect) ->addFilterByRequiredOptions() ->setStoreId($product->getStoreId()); diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php index b013916cc221a..aa801088de7fd 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php @@ -113,11 +113,12 @@ public function afterSave() } /** - * Load configurable attribute by product and product's attribute + * Load configurable attribute by product and product's attribute. * * @param \Magento\Catalog\Model\Product $product * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @throws LocalizedException + * @return void */ public function loadByProductAndAttribute($product, $attribute) { diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php index e8b7299a03db9..cb4ac975cd582 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Plugin.php @@ -6,7 +6,7 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; /** * Type plugin. @@ -14,14 +14,14 @@ class Plugin { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager */ - public function __construct(ModuleManagerInterface $moduleManager) + public function __construct(Manager $moduleManager) { $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php index 979587dc500a4..f837444aa45ca 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Model\Product\Type; /** + * Variation matrix. + * * @api * @since 100.0.2 */ @@ -40,7 +43,9 @@ public function getVariations($usedProductAttributes) for ($attributeIndex = $attributesCount; $attributeIndex--;) { $currentAttribute = $variationalAttributes[$attributeIndex]; $currentVariationValue = $currentVariation[$attributeIndex]; - $filledVariation[$currentAttribute['id']] = $currentAttribute['values'][$currentVariationValue]; + if (!empty($currentAttribute['id'])) { + $filledVariation[$currentAttribute['id']] = $currentAttribute['values'][$currentVariationValue]; + } } $variations[] = $filledVariation; diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php index 8f2cc6ddb43ce..57f701721a6f3 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php @@ -302,7 +302,9 @@ protected function _loadLabels() } /** - * Load attribute options. + * Load related options' data. + * + * @return void */ protected function loadOptions() { @@ -329,6 +331,7 @@ protected function loadOptions() 'use_default_value' => true ]; } + $item->setOptionsMap($values); $values = array_values($values); $item->setOptions($values); } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php index 1ed4432347b7a..c97b24c295189 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php @@ -56,7 +56,10 @@ private function getProductToValidate( $attrCode = $subject->getAttribute(); /* Check for attributes which are not available for configurable products */ - if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) { + if ($product->getTypeId() == Configurable::TYPE_CODE && + !$product->hasData($attrCode) && + count($model->getChildren()) + ) { /** @var \Magento\Catalog\Model\AbstractModel $childProduct */ $childProduct = current($model->getChildren())->getProduct(); if ($childProduct->hasData($attrCode)) { diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminChangeConfigurableProductVariationQtyActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminChangeConfigurableProductVariationQtyActionGroup.xml new file mode 100644 index 0000000000000..3441b979226dd --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminChangeConfigurableProductVariationQtyActionGroup.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="AdminChangeConfigurableProductVariationQty"> + <annotations> + <description>Change quantity value for configurable product generated variation</description> + </annotations> + <arguments> + <argument name="rowIndex" type="string" defaultValue="0"/> + <argument name="quantity" type="string" defaultValue="0"/> + </arguments> + <fillField selector="{{AdminCreateProductConfigurationsPanel.variationQty(rowIndex)}}" userInput="{{quantity}}" stepKey="fillVariationQuantity"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 488667a4585bb..02c7aeb3db6ac 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -8,6 +8,47 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateConfigurableProductWithAttributeSet"> + <annotations> + <description>Admin edit created product as configurable. Choose created options</description> + </annotations> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + <argument name="label" type="string" defaultValue="mySet"/> + <argument name="option" type="string" defaultValue="['option1', 'option2', 'option3', 'option4']"/> + </arguments> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{label}}" stepKey="searchForAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <selectOption selector="{{AdminProductFormSection.additionalOptions}}" parameterArray="{{option}}" stepKey="searchAndMultiSelectCreatedOption"/> + </actionGroup> + <actionGroup name="AdminCreateConfigurationsForAttribute" extends="generateConfigurationsByAttributeCode"> + <annotations> + <description>EXTENDS: generateConfigurationsByAttributeCode. Click to apply single price to all Skus. Enter Attribute price</description> + </annotations> + <arguments> + <argument name="price" type="string" defaultValue="100"/> + </arguments> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" after="clickOnNextButton2" stepKey="waitForNextPageOpened2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" after="waitForNextPageOpened2" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="{{price}}" before="clickOnApplySingleQuantityToEachSku" stepKey="enterAttributePrice"/> + </actionGroup> + <actionGroup name="AdminCreateConfigurableProductWithAttributeUncheckOption" extends="generateConfigurationsByAttributeCode"> + <annotations> + <description>EXTENDS: generateConfigurationsByAttributeCode. Click to uncheck created option. Enter Attribute price</description> + </annotations> + <arguments> + <argument name="attributeOption" type="string" defaultValue="option1"/> + <argument name="price" type="string" defaultValue="100"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeOption('attributeOption')}}" after="clickOnSelectAll" stepKey="clickToUncheckOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" after="clickToUncheckOption" stepKey="clickOnNextButton22"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" after="clickOnNextButton22" stepKey="waitForNextPageOpened2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" after="waitForNextPageOpened2" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="{{price}}" before="clickOnApplySingleQuantityToEachSku" stepKey="enterAttributePrice"/> + </actionGroup> <!--Filter the product grid and view expected products--> <actionGroup name="viewConfigurableProductInAdminGrid"> <annotations> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFilterAttributeInConfigurableAttributesGridActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFilterAttributeInConfigurableAttributesGridActionGroup.xml new file mode 100644 index 0000000000000..56a70a0ba29f9 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFilterAttributeInConfigurableAttributesGridActionGroup.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="AdminFilterAttributeInConfigurableAttributesGrid"> + <annotations> + <description>Filter attribute in configurable attributes grid by attribute code value</description> + </annotations> + <arguments> + <argument name="attributeCode" type="string" defaultValue="{{newProductAttribute.attribute_code}}"/> + </arguments> + <conditionalClick selector="{{AdminDataGridFilterSection.clear}}" visible="true" dependentSelector="{{AdminDataGridFilterSection.clear}}" stepKey="clearFilters"/> + <waitForPageLoad stepKey="waitForFiltersReset"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="waitForFiltersAppear"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="expandFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{attributeCode}}" stepKey="fillFilterValue"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectAttributeInConfigurableAttributesGridActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectAttributeInConfigurableAttributesGridActionGroup.xml new file mode 100644 index 0000000000000..72ab16df87dac --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectAttributeInConfigurableAttributesGridActionGroup.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="AdminSelectAttributeInConfigurableAttributesGrid" extends="AdminFilterAttributeInConfigurableAttributesGrid"> + <annotations> + <description>EXTENDS: AdminFilterAttributeInConfigurableAttributesGrid. Select first filtered attribute.</description> + </annotations> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="checkAttributeInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml index de6714a9b959e..3edb7c0f17ab9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml @@ -60,10 +60,38 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiConfigurableProductWithDescriptionUnderscoredSku" type="product"> + <data key="sku" unique="suffix">api_configurable_product</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">API Configurable Product</data> + <data key="urlKey" unique="suffix">api-configurable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> <entity name="ConfigurableProductAddChild" type="ConfigurableProductAddChild"> <var key="sku" entityKey="sku" entityType="product" /> <var key="childSku" entityKey="sku" entityType="product2"/> </entity> + <entity name="ConfigurableProductWithAttributeSet" type="product"> + <data key="sku" unique="suffix">configurable</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="attribute_set_name">mySet</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Configurable product</data> + <data key="price">1.00</data> + <data key="weight">2</data> + <data key="urlKey" unique="suffix">configurableurlkey</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="ConfigurableProductPrice10Qty1" type="product"> <data key="sku" unique="suffix">configurable-product_</data> <data key="type_id">configurable</data> @@ -78,4 +106,10 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <!-- Configurable product from file "export_import_configurable_product.csv"--> + <entity name="ApiConfigurableExportImportProduct" extends="ApiConfigurableProduct" type="product"> + <data key="sku">api-configurable-export-import-product</data> + <data key="name">API Configurable Export Import Product</data> + <data key="urlKey">api-configurable-export-import-product</data> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index f3c628d002e7a..92e2450ef4f3d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreateProductConfigurationsPanel"> + <element name="attributeOption" type="checkbox" selector="li[data-attribute-option-title='{{colorOption}}']" parameterized="true"/> <element name="next" type="button" selector=".steps-wizard-navigation .action-next-step" timeout="30"/> <element name="createNewAttribute" type="button" selector=".select-attributes-actions button[title='Create New Attribute']" timeout="30"/> <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> @@ -16,6 +17,8 @@ <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> <element name="id" type="text" selector="//tr[contains(@data-repeat-index, '0')]/td[2]/div"/> + <element name="variationsGrid" type="block" selector=".admin__field[data-index='configurable-matrix']"/> + <element name="variationQty" type="input" selector=".admin__field[data-index='configurable-matrix'] input[name='configurable-matrix[{{rowIndex}}][qty]']" parameterized="true"/> <element name="attributeCheckbox" type="checkbox" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//input[@data-action='select-row']" parameterized="true"/> <element name="defaultLabel" type="text" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//td[3]/div[@class='data-grid-cell-content']" parameterized="true"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml index 1defecbc7c285..336f95aa55576 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -39,6 +39,9 @@ <element name="variationLabel" type="text" selector="//div[@data-index='configurable-matrix']/label"/> <element name="stepsWizardTitle" type="text" selector="div.content:not([style='display: none;']) .steps-wizard-title"/> <element name="attributeEntityByName" type="text" selector="//div[@class='attribute-entity']//div[normalize-space(.)='{{attributeLabel}}']" parameterized="true"/> + <element name="fileUploaderInput" type="file" selector="//input[@type='file' and @class='file-uploader-input']" /> + <element name="variationImageSource" type="text" selector="[data-index='configurable-matrix'] [data-index='thumbnail_image_container'] img[src*='{{imageName}}']" parameterized="true"/> + <element name="variationProductLinkByName" type="text" selector="//div[@data-index='configurable-matrix']//*[@data-index='name_container']//a[contains(text(), '{{productName}}')]" parameterized="true"/> </section> <section name="AdminConfigurableProductFormSection"> <element name="productWeight" type="input" selector=".admin__control-text[name='product[weight]']"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml index a9d2f1c3379df..36c135c427365 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -29,6 +29,11 @@ <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> + <argument name="productName" value="$$createConfigProductAttributeCreateConfigurableProduct.name$$"/> + </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml index 68bf703ecdab4..c085229da8028 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml @@ -69,7 +69,7 @@ <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> <!-- Assert product auto incremented sku notice message; see success message --> - <see selector="{{AdminMessagesSection.noticeMessage}}" stepKey="seeNoticeMessage" userInput="SKU for product {{ApiConfigurableProduct.name}} has been changed to {{ApiConfigurableProduct.sku}}-2."/> - <see selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage" userInput="You saved the product."/> + <see selector="{{AdminMessagesSection.notice}}" stepKey="seeNoticeMessage" userInput="SKU for product {{ApiConfigurableProduct.name}} has been changed to {{ApiConfigurableProduct.sku}}-2."/> + <see selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage" userInput="You saved the product."/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml new file mode 100644 index 0000000000000..de0cca11235ea --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -0,0 +1,203 @@ +<?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="AdminCheckResultsOfColorAndOtherFiltersTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Checking filters results"/> + <title value="Checking results of color and other filters"/> + <description value="Checking results of filters: color and other filters"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6192"/> + <useCaseId value="MAGETWO-91753"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <!-- Create default category with subcategory --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SubCategoryWithParent" stepKey="createSubcategory"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create three configurable product --> + <createData entity="ConfigurableProductWithAttributeSet" stepKey="createFirstConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ConfigurableProductWithAttributeSet" stepKey="createSecondConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ConfigurableProductWithAttributeSet" stepKey="createThirdConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Add first attribute with options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption4" stepKey="createConfigProductAttributeOption4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption5" stepKey="createConfigProductAttributeOption5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add second attribute with options--> + <createData entity="multipleSelectProductAttribute" stepKey="createConfigProductAttribute2"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption12"> + <requiredEntity createDataKey="createConfigProductAttribute2"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption6"> + <requiredEntity createDataKey="createConfigProductAttribute2"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption7"> + <requiredEntity createDataKey="createConfigProductAttribute2"/> + </createData> + <createData entity="productAttributeOption4" stepKey="createConfigProductAttributeOption8"> + <requiredEntity createDataKey="createConfigProductAttribute2"/> + </createData> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Add created attributes with options to Attribute Set --> + <actionGroup ref="AdminAddUnassignedAttributeToGroup" stepKey="createDefaultAttributeSet"> + <argument name="label" value="mySet"/> + <argument name="firstOption" value="$$createConfigProductAttribute.attribute_code$$"/> + <argument name="secondOption" value="$$createConfigProductAttribute2.attribute_code$$"/> + <argument name="group" value="Product Details"/> + </actionGroup> + </before> + <after> + <!-- Delete all created data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstConfigurableProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondConfigurableProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdConfigurableProduct" stepKey="deleteThirdProduct"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!-- Delete attribute set --> + <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> + <argument name="label" value="mySet"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearAttributeSetsFilter"/> + <!-- Delete First attribute --> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <!-- Delete Second attribute --> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openSecondProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="$$createConfigProductAttribute2.attribute_code$$"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteSecondProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="$$createConfigProductAttribute2.attribute_code$$"/> + </actionGroup> + <!-- Clear filters --> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductAttributesFilter"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilter"/> + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create three configurable products with options --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <!-- Edit created first product as configurable product with options --> + <actionGroup ref="filterProductGridBySku" stepKey="filterGridByFirstProduct"> + <argument name="product" value="$$createFirstConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductFirst"> + <argument name="product" value="$$createFirstConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="CreateConfigurableProductWithAttributeSet" stepKey="createProductFirst"> + <argument name="product" value="$$createFirstConfigurableProduct$$"/> + <argument name="category" value="$$createCategory$$"/> + <argument name="label" value="mySet"/> + <argument name="option1" value="['option1', 'option2', 'option3', 'option4']"/> + </actionGroup> + <actionGroup ref="AdminCreateConfigurationsForAttribute" stepKey="createConfigurationFirst"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + <argument name="price" value="34"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitBtnFirst" /> + <click selector="{{CmsNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveAndCloseFirst"/> + <waitForPageLoad stepKey="waitForMessage"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageFirst"/> + <!-- Edit created second product as configurable product with options --> + <actionGroup ref="filterProductGridBySku" stepKey="filterGridBySecondProduct"> + <argument name="product" value="$$createSecondConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductSecond"> + <argument name="product" value="$$createSecondConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="CreateConfigurableProductWithAttributeSet" stepKey="createProductSecond"> + <argument name="product" value="$$createSecondConfigurableProduct$$"/> + <argument name="category" value="$$createCategory$$"/> + <argument name="label" value="mySet"/> + <argument name="option1" value="['option1', 'option2', 'option3']"/> + </actionGroup> + <actionGroup ref="AdminCreateConfigurableProductWithAttributeUncheckOption" stepKey="createConfigurationSecond"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + <argument name="price" value="34"/> + <argument name="attributeOption" value="option5"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoadThird"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitBtnSecond" /> + <click selector="{{CmsNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveAndCloseSecond"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageSecond"/> + <!-- Edit created third product as configurable product with options --> + <actionGroup ref="filterProductGridBySku" stepKey="filterGridByThirdProduct"> + <argument name="product" value="$$createThirdConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductThird"> + <argument name="product" value="$$createThirdConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="CreateConfigurableProductWithAttributeSet" stepKey="createProductThird"> + <argument name="product" value="$$createThirdConfigurableProduct$$"/> + <argument name="category" value="$$createCategory$$"/> + <argument name="label" value="mySet"/> + <argument name="option1" value="['option2', 'option3', 'option4']"/> + </actionGroup> + <actionGroup ref="AdminCreateConfigurableProductWithAttributeUncheckOption" stepKey="createConfigurationThird"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + <argument name="price" value="34"/> + <argument name="attributeOption" value="option1"/> + </actionGroup> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitBtnThird" /> + <click selector="{{CmsNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveAndCloseThird"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveConfigurableProductMessage"/> + <!-- Create Simple product with options --> + <actionGroup ref="filterProductGridBySku" stepKey="filterGridBySimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="CreateConfigurableProductWithAttributeSet" stepKey="createSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="category" value="$$createCategory$$"/> + <argument name="label" value="mySet"/> + <argument name="option1" value="['option1', 'option2']"/> + </actionGroup> + <click selector="{{AdminGridMainControls.save}}" stepKey="clickToSaveProduct"/> + <waitForPageLoad stepKey="waitForNewSimpleProductPage"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageThird"/> + <!--TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml index 9021bf981ac13..430007ae761f7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml @@ -26,7 +26,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a configurable product via the UI --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml index 1a694b8adf17e..33a6da9dabf34 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml @@ -66,7 +66,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> @@ -216,7 +216,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index c599a6a23f190..cd09cbd295877 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -52,7 +52,7 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!--Clean up category--> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create a configurable product with long name and sku--> @@ -87,6 +87,9 @@ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="seeChildProductSku2"/> <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Assert storefront category list page--> <amOnPage url="/" stepKey="amOnStorefront"/> <waitForPageLoad stepKey="waitForStorefrontLoad"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml index ad30c91967c32..51b3e49f51913 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml @@ -77,7 +77,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> @@ -200,7 +200,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> @@ -301,7 +301,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml index 059a18200e90c..410c85d314904 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml @@ -68,7 +68,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> @@ -147,7 +147,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml index 00ffe70380d18..dba481b64810a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml @@ -96,7 +96,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <!-- Delete everything that was created in the before block --> <deleteData createDataKey="createCategory" stepKey="deleteCatagory" /> @@ -213,7 +213,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <!-- Delete everything that was created in the before block --> <deleteData createDataKey="createCategory" stepKey="deleteCatagory" /> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml index 3a6a20de3ed90..7c6cd57097591 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml @@ -17,9 +17,6 @@ <testCaseId value="MC-88"/> <group value="ConfigurableProduct"/> <severity value="AVERAGE"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> @@ -37,14 +34,20 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct1" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createProduct2" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createProduct3" stepKey="deleteThirdProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="logout"/> </after> <!-- Search for prefix of the 3 products we created via api --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> <waitForPageLoad stepKey="wait1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clearAll"/> <actionGroup ref="searchProductGridByKeyword" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> </actionGroup> @@ -59,25 +62,25 @@ <!-- Update the description --> <click selector="{{AdminUpdateAttributesSection.toggleDescription}}" stepKey="clickToggleDescription"/> <fillField selector="{{AdminUpdateAttributesSection.description}}" userInput="MFTF automation!" stepKey="fillDescription"/> - <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="clickSave"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" time="60" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpdateSuccessMsg"/> <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron1"/> + <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> <!-- Check storefront for description --> - <amOnPage url="$$createProduct1.sku$$.html" stepKey="gotoProduct1"/> - <waitForPageLoad stepKey="wait3"/> - <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="MFTF automation!" stepKey="seeDescription1"/> - <amOnPage url="$$createProduct2.sku$$.html" stepKey="gotoProduct2"/> - <waitForPageLoad stepKey="wait4"/> - <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="MFTF automation!" stepKey="seeDescription2"/> - <amOnPage url="$$createProduct3.sku$$.html" stepKey="gotoProduct3"/> - <waitForPageLoad stepKey="wait5"/> - <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="MFTF automation!" stepKey="seeDescription3"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct1.custom_attributes[url_key]$$)}}" stepKey="goToFirstProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="MFTF automation!" stepKey="seeFirstDescription"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct2.custom_attributes[url_key]$$)}}" stepKey="goToSecondProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="MFTF automation!" stepKey="seeSecondDescription"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct3.custom_attributes[url_key]$$)}}" stepKey="goToThirdProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForThirdProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="MFTF automation!" stepKey="seeThirdDescription"/> </test> <test name="AdminConfigurableProductRemoveAnOptionTest"> @@ -277,7 +280,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a configurable product via the UI --> @@ -326,7 +329,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a configurable product via the UI --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index 925e7a890cead..9796c14f5519a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -129,6 +129,9 @@ <!-- Save product --> <actionGroup ref="saveConfigurableProductAddToCurrentAttributeSet" stepKey="saveProduct"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml new file mode 100644 index 0000000000000..fa21d20eb4456 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml @@ -0,0 +1,181 @@ +<?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="AdminSimpleProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product type switching"/> + <title value="Simple product type switching on editing to configurable product"/> + <description value="Simple product type switching on editing to configurable product"/> + <testCaseId value="MAGETWO-29633"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!--Create attribute with options--> + <comment userInput="Create attribute with options" stepKey="commentCreateAttributeWithOptions"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Add configurations to product--> + <comment userInput="Add configurations to product" stepKey="commentAddConfigs"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="setupConfigurations"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveConfigProductForm"/> + <!--Assert configurable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertConfigProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Name')}}" userInput="$$createProduct.name$$-option1" stepKey="seeProductNameInGrid1"/> + <see selector="{{AdminProductGridSection.productGridCell('3', 'Name')}}" userInput="$$createProduct.name$$-option2" stepKey="seeProductNameInGrid2"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <!--Assert configurable product on storefront--> + <comment userInput="Assert configurable product on storefront" stepKey="commentAssertConfigProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertInStock"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="clickAttributeDropDown"/> + <see userInput="option1" stepKey="verifyOption1Exists"/> + <see userInput="option2" stepKey="verifyOption2Exists"/> + </test> + <test name="AdminConfigurableProductTypeSwitchingToVirtualProductTest" extends="AdminSimpleProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product type switching"/> + <title value="Configurable product type switching on editing to virtual product"/> + <description value="Configurable product type switching on editing to virtual product"/> + <testCaseId value="MC-17952"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <!--Delete product configurations--> + <comment userInput="Delete product configuration" stepKey="commentDeleteConfigs"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToConfigProductPage"/> + <waitForPageLoad stepKey="waitForConfigurableProductPageLoad"/> + <conditionalClick selector="{{ AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandOption1Actions"/> + <click selector="{{AdminProductFormConfigurationsSection.removeProductBtn}}" stepKey="clickRemoveOption1"/> + <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandOption2Actions"/> + <click selector="{{AdminProductFormConfigurationsSection.removeProductBtn}}" stepKey="clickRemoveOption2"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{SimpleProduct2.price}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{SimpleProduct2.quantity}}" stepKey="fillProductQty"/> + <clearField selector="{{AdminProductFormSection.productWeight}}" stepKey="clearWeightField"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has no weight" stepKey="selectNoWeight"/> + <actionGroup ref="saveProductForm" stepKey="saveVirtualProductForm"/> + <!--Assert virtual product on Admin product page grid--> + <comment userInput="Assert virtual product on Admin product page grid" stepKey="commentAssertVirtualProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPageForVirtual"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySkuForVirtual"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeVirtualProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeVirtualProductTypeInGrid"/> + <!--Assert virtual product on storefront--> + <comment userInput="Assert virtual product on storefront" stepKey="commentAssertVirtualProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openVirtualProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontVirtualProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertVirtualProductInStock"/> + </test> + <test name="AdminVirtualProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product type switching"/> + <title value="Virtual product type switching on editing to configurable product"/> + <description value="Virtual product type switching on editing to configurable product"/> + <testCaseId value="MC-17953"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="VirtualProduct" stepKey="createProduct"/> + <!--Create attribute with options--> + <comment userInput="Create attribute with options" stepKey="commentCreateAttributeWithOptions"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Add configurations to product--> + <comment userInput="Add configurations to product" stepKey="commentAddConfigurations"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToConfigProductPage"/> + <waitForPageLoad stepKey="waitForConfigurableProductPageLoad"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeightForConfigurableProduct"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="setupConfigurationsForProduct"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveNewConfigurableProductForm"/> + <!--Assert configurable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertConfigurableProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPageForConfigurable"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySkuForConfigurable"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeConfigurableProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Configurable Product" stepKey="seeConfigurableProductTypeInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Name')}}" userInput="$$createProduct.name$$-option1" stepKey="seeConfigurableProductNameInGrid1"/> + <see selector="{{AdminProductGridSection.productGridCell('3', 'Name')}}" userInput="$$createProduct.name$$-option2" stepKey="seeConfigurableProductNameInGrid2"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearConfigurableProductFilters"/> + <!--Assert configurable product on storefront--> + <comment userInput="Assert configurable product on storefront" stepKey="commentAssertConfigurableProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openConfigurableProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontConfigurableProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertConfigurableProductInStock"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="clickConfigurableAttributeDropDown"/> + <see userInput="option1" stepKey="verifyConfigurableProductOption1Exists"/> + <see userInput="option2" stepKey="verifyConfigurableProductOption2Exists"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml index c303e4d19db81..0370280309272 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml @@ -80,6 +80,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> <test name="AdvanceCatalogSearchConfigurableBySkuTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> @@ -104,7 +106,7 @@ </createData> <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> - <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> + <createData entity="ApiConfigurableProductWithDescriptionUnderscoredSku" stepKey="product"/> <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> @@ -154,6 +156,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> <test name="AdvanceCatalogSearchConfigurableByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> @@ -228,6 +232,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> <test name="AdvanceCatalogSearchConfigurableByShortDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> @@ -302,6 +308,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml index a71f51526c8ab..83d9bbe8c270a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml @@ -77,7 +77,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 47ee09e4b2086..04687a2314dc6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -217,4 +217,213 @@ <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="compareGrabConfigProductImageSrcInComparison" after="compareAssertConfigProductInComparison"/> <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabConfigProductImageSrcInComparison" stepKey="compareAssertConfigProductImageNotDefaultInComparison" after="compareGrabConfigProductImageSrcInComparison"/> </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <before> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigChildProduct1Image"> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createConfigChildProduct2Image"> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigProductImage"> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateConfigProduct" createDataKey="createConfigProduct"/> + </before> + <after> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createConfigChildProduct1Image" stepKey="deleteConfigChildProduct1Image"/>--> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createConfigChildProduct2Image" stepKey="deleteConfigChildProduct2Image"/>--> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createConfigProductImage" stepKey="deleteConfigProductImage"/>--> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Verify Configurable Product in checkout cart items --> + <comment userInput="Verify Configurable Product in checkout cart items" stepKey="commentVerifyConfigurableProductInCheckoutCartItems" after="guestCheckoutCheckSimpleProduct2InCartItems" /> + <actionGroup ref="CheckConfigurableProductInCheckoutCartItemsActionGroup" stepKey="guestCheckoutCheckConfigurableProductInCartItems" after="commentVerifyConfigurableProductInCheckoutCartItems"> + <argument name="productVar" value="$$createConfigProduct$$"/> + <argument name="optionLabel" value="$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$" /> + <argument name="optionValue" value="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" /> + </actionGroup> + + <!-- Check configurable product in category --> + <comment userInput="Verify Configurable Product in category" stepKey="commentVerifyConfigurableProductInCategory" after="browseAssertSimpleProduct2ImageNotDefault" /> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="browseAssertCategoryConfigProduct" after="commentVerifyConfigurableProductInCategory"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="browseGrabConfigProductImageSrc" after="browseAssertCategoryConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$browseGrabConfigProductImageSrc" stepKey="browseAssertConfigProductImageNotDefault" after="browseGrabConfigProductImageSrc"/> + + <!-- View Configurable Product --> + <comment userInput="View Configurable Product" stepKey="commentViewConfigurableProduct" after="browseAssertSimpleProduct2PageImageNotDefault" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickCategory2" after="commentViewConfigurableProduct"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$)}}" stepKey="browseClickCategoryConfigProductView" after="clickCategory2"/> + <waitForLoadingMaskToDisappear stepKey="waitForConfigurableProductViewloaded" after="browseClickCategoryConfigProductView"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="browseAssertConfigProductPage" after="waitForConfigurableProductViewloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="browseGrabConfigProductPageImageSrc" after="browseAssertConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$browseGrabConfigProductPageImageSrc" stepKey="browseAssertConfigProductPageImageNotDefault" after="browseGrabConfigProductPageImageSrc"/> + + <!-- Add Configurable Product to cart --> + <comment userInput="Add Configurable Product to cart" stepKey="commentAddConfigurableProductToCart" after="cartAddProduct2ToCart" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory2" after="commentAddConfigurableProductToCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartCategory2loaded" after="cartClickCategory2"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="cartAssertCategory1ForConfigurableProduct" after="waitForCartCategory2loaded"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="cartAssertConfigProduct" after="cartAssertCategory1ForConfigurableProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="cartGrabConfigProductImageSrc" after="cartAssertConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartGrabConfigProductImageSrc" stepKey="cartAssertConfigProductImageNotDefault" after="cartGrabConfigProductImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName($$createConfigProduct.name$$)}}" stepKey="cartClickCategoryConfigProductAddToCart" after="cartAssertConfigProductImageNotDefault"/> + <waitForElement selector="{{StorefrontMessagesSection.message('You need to choose options for your item.')}}" time="30" stepKey="cartWaitForConfigProductPageLoad" after="cartClickCategoryConfigProductAddToCart"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertConfigProductPage" after="cartWaitForConfigProductPageLoad"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartGrabConfigProductPageImageSrc1" after="cartAssertConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartGrabConfigProductPageImageSrc1" stepKey="cartAssertConfigProductPageImageNotDefault1" after="cartGrabConfigProductPageImageSrc1"/> + <selectOption userInput="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="cartConfigProductFillOption" after="cartAssertConfigProductPageImageNotDefault1"/> + <waitForLoadingMaskToDisappear stepKey="waitForConfigurableProductOptionloaded" after="cartConfigProductFillOption"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertConfigProductWithOptionPage" after="waitForConfigurableProductOptionloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartGrabConfigProductPageImageSrc2" after="cartAssertConfigProductWithOptionPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartGrabConfigProductPageImageSrc2" stepKey="cartAssertConfigProductPageImageNotDefault2" after="cartGrabConfigProductPageImageSrc2"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddConfigProductToCart" after="cartAssertConfigProductPageImageNotDefault2"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + + <!-- Check configurable product in minicart --> + <comment userInput="Check configurable product in minicart" stepKey="commentCheckConfigurableProductInMinicart" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> + <actionGroup ref="StorefrontOpenMinicartAndCheckConfigurableProductActionGroup" stepKey="cartOpenMinicartAndCheckConfigProduct" after="commentCheckConfigurableProductInMinicart"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="cartMinicartGrabConfigProductImageSrc" after="cartOpenMinicartAndCheckConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/thumbnail\.jpg/'" actual="$cartMinicartGrabConfigProductImageSrc" stepKey="cartMinicartAssertConfigProductImageNotDefault" after="cartMinicartGrabConfigProductImageSrc"/> + <click selector="{{StorefrontMinicartSection.productOptionsDetailsByName($$createConfigProduct.name$$)}}" stepKey="cartMinicartClickConfigProductDetails" after="cartMinicartAssertConfigProductImageNotDefault"/> + <see userInput="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" selector="{{StorefrontMinicartSection.productOptionByNameAndAttribute($$createConfigProduct.name$$, $$createConfigProductAttribute.attribute[frontend_labels][0][label]$$)}}" stepKey="cartMinicartCheckConfigProductOption" after="cartMinicartClickConfigProductDetails"/> + <click selector="{{StorefrontMinicartSection.productLinkByName($$createConfigProduct.name$$)}}" stepKey="cartMinicartClickConfigProduct" after="cartMinicartCheckConfigProductOption"/> + <waitForLoadingMaskToDisappear stepKey="waitForMinicartConfigProductloaded" after="cartMinicartClickConfigProduct"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertMinicartConfigProductPage" after="waitForMinicartConfigProductloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartMinicartGrabConfigProductPageImageSrc" after="cartAssertMinicartConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartMinicartGrabConfigProductPageImageSrc" stepKey="cartMinicartAssertConfigProductPageImageNotDefault" after="cartMinicartGrabConfigProductPageImageSrc"/> + + <!-- Check configurable product in cart --> + <comment userInput="Check configurable product in cart" stepKey="commentCheckConfigurableProductInCart" after="cartCartAssertSimpleProduct2PageImageNotDefault2" /> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart2" after="commentCheckConfigurableProductInCart"/> + <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="cartAssertCartConfigProduct" after="cartOpenCart2"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct2$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{CheckoutCartProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="cartCartGrabConfigProduct2ImageSrc" after="cartAssertCartConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartCartGrabConfigProduct2ImageSrc" stepKey="cartCartAssertConfigProduct2ImageNotDefault" after="cartCartGrabConfigProduct2ImageSrc"/> + <see userInput="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute($$createConfigProduct.name$$, $$createConfigProductAttribute.attribute[frontend_labels][0][label]$$)}}" stepKey="cartCheckConfigProductOption" after="cartCartAssertConfigProduct2ImageNotDefault"/> + <click selector="{{CheckoutCartProductSection.ProductLinkByName($$createConfigProduct.name$$)}}" stepKey="cartClickCartConfigProduct" after="cartCheckConfigProductOption"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartConfigProductloaded" after="cartClickCartConfigProduct"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertCartConfigProductPage" after="waitForCartConfigProductloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartCartGrabConfigProductPageImageSrc" after="cartAssertCartConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartCartGrabConfigProductPageImageSrc" stepKey="cartCartAssertConfigProductPageImageNotDefault" after="cartCartGrabConfigProductPageImageSrc"/> + + <!-- Add Configurable Product to comparison --> + <comment userInput="Add Configurable Product to comparison" stepKey="commentAddConfigurableProductToComparison" after="compareAddSimpleProduct2ToCompare" /> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="compareAssertConfigProduct" after="commentAddConfigurableProductToComparison"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="compareGrabConfigProductImageSrc" after="compareAssertConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabConfigProductImageSrc" stepKey="compareAssertConfigProductImageNotDefault" after="compareGrabConfigProductImageSrc"/> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="compareAddConfigProductToCompare" after="compareAssertConfigProductImageNotDefault"> + <argument name="productVar" value="$$createConfigProduct$$"/> + </actionGroup> + + <!-- Check configurable product in comparison sidebar --> + <comment userInput="Add Configurable Product in comparison sidebar" stepKey="commentAddConfigurableProductInComparisonSidebar" after="compareSimpleProduct2InSidebar" /> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareConfigProductInSidebar" after="commentAddConfigurableProductInComparisonSidebar"> + <argument name="productVar" value="$$createConfigProduct$$"/> + </actionGroup> + + <!-- Check configurable product on comparison page --> + <comment userInput="Add Configurable Product on comparison page" stepKey="commentAddConfigurableProductOnComparisonPage" after="compareAssertSimpleProduct2ImageNotDefaultInComparison" /> + <actionGroup ref="StorefrontCheckCompareConfigurableProductActionGroup" stepKey="compareAssertConfigProductInComparison" after="commentAddConfigurableProductOnComparisonPage"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="compareGrabConfigProductImageSrcInComparison" after="compareAssertConfigProductInComparison"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabConfigProductImageSrcInComparison" stepKey="compareAssertConfigProductImageNotDefaultInComparison" after="compareGrabConfigProductImageSrcInComparison"/> + </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml new file mode 100644 index 0000000000000..2d0c4a05c1dec --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml @@ -0,0 +1,69 @@ +<?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="NoErrorForMiniCartItemEditTest"> + <annotations> + <features value="ConfigurableProduct"/> + <title value="No error for minicart item edit test"/> + <description value="Already selected configurable option should be selected when configurable product is edited from minicart"/> + <severity value="MAJOR"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create Configurable product --> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete the first simple product --> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" + stepKey="clickClearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go To Created Product Page --> + <amOnPage stepKey="goToCreatedProductPage" url="{{_defaultProduct.urlKey}}.html"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <!-- Add Product to Cart --> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" + stepKey="checkDropDownProductOption"/> + <selectOption userInput="{{colorProductAttribute1.name}}" + selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + stepKey="selectOption1"/> + <selectOption userInput="{{colorProductAttribute2.name}}" + selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + stepKey="selectOption2"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" + stepKey="clickDropDownProductOption"/> + <selectOption userInput="{{colorProductAttribute1.name}}" + selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + stepKey="selectOptionForAddingToCart"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> + <waitForPageLoad stepKey="waitForMiniCart"/> + + <!-- Edit Item in Cart --> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> + <click selector="{{StorefrontMinicartSection.editMiniCartItem}}" stepKey="clickEditCartItem"/> + + <!-- Check if Product Configuration is still selected --> + <see selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + userInput="{{colorProductAttribute1.name}}" stepKey="seeConfigurationSelected"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml new file mode 100644 index 0000000000000..fd607d2203c66 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml @@ -0,0 +1,160 @@ +<?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="NoOptionAvailableToConfigureDisabledProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Admin order configurable product"/> + <title value="Disabled variation of configurable product can't be added to shopping cart via admin"/> + <description value="Disabled variation of configurable product can't be added to shopping cart via admin"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-17373"/> + <useCaseId value="MAGETWO-72172"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create category--> + <comment userInput="Create category" stepKey="commentCreateCategory"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!-- Create the configurable product based on the data in the data folder --> + <comment userInput="Create the configurable product based on the data in the data folder" stepKey="createConfigurableProduct"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create the configurable product with two options based on the default attribute set --> + <comment userInput="Create the configurable product with two options based on the default attribute set" stepKey="configurableProductWithTwoOptions"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create the 3 children that will be a part of the configurable product --> + <comment userInput="Create the 3 children that will be a part of the configurable product" stepKey="createTwoChildrenProducts"/> + <createData entity="ApiSimpleProductWithPrice50" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleProductWithPrice60" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiSimpleProductWithPrice70" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <!-- Assign 3 products to the configurable product --> + <comment userInput="Assign 3 products to the configurable product" stepKey="assignToConfigurableProduct"/> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <!-- Create Customer --> + <comment userInput="Create customer" stepKey="commentCreateCustomer"/> + <createData entity="Simple_US_Customer_CA" stepKey="createCustomer"/> + </before> + <after> + <!-- Delete created data --> + <comment userInput="Delete created data" stepKey="deleteData"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory2"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Disable child product --> + <comment userInput="Disable child product" stepKey="disableChildProduct"/> + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct1.id$$)}}" stepKey="goToEditPage"/> + <waitForPageLoad stepKey="waitForChildProductPageLoad"/> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + <!-- Set the second product out of stock --> + <comment userInput="Set the second product out of stock" stepKey="outOfStockChildProduct"/> + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct2.id$$)}}" stepKey="goToSecondProductEditPage"/> + <waitForPageLoad stepKey="waitForSecondChildProductPageLoad"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="outOfStockStatus"/> + <actionGroup ref="saveProductForm" stepKey="saveSecondProductForm"/> + <!-- Go to created customer page --> + <comment userInput="Go to created customer page" stepKey="goToCreatedCustomerPage"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrder"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct"/> + <waitForPageLoad stepKey="waitForProductsOpened"/> + <!-- Find created configurable product and click on "Configure" link --> + <comment userInput="Find created configurable product and click on Configure link" stepKey="goToConfigurableLink"/> + <click selector="{{AdminOrderFormConfigureProductSection.configure($$createConfigProduct.id$$)}}" stepKey="clickOnConfigure"/> + <!-- Click on attribute drop-down and check no option 1 is available --> + <comment userInput="Click on attribute drop-down and check no option 1 is available" stepKey="commentNoOptionIsAvailable"/> + <waitForElement selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="waitForShippingSectionLoaded"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="clickToSelectOption"/> + <dontSee userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" stepKey="dontSeeOption1"/> + <!-- Go to created customer page again --> + <comment userInput="Go to created customer page again" stepKey="goToCreatedCustomerPageAgain"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrderAgain"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProductAgain"/> + <waitForPageLoad stepKey="waitForProductsOpenedAgain"/> + <fillField selector="{{AdminOrderFormItemsSection.idFilter}}" userInput="$$createConfigChildProduct2.id$$" stepKey="idFilter"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearch"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectConfigurableProduct"/> + <!-- Add product to order --> + <comment userInput="Add product to order" stepKey="addProductToOrder"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickToAddProductToOrder"/> + <waitForPageLoad stepKey="waitForNewOrderPageLoad"/> + <see userInput="This product is out of stock." stepKey="seeTheErrorMessageDisplayed"/> + <!-- Select shipping method --> + <comment userInput="Select shipping method" stepKey="selectShippingMethod"/> + <click selector="{{AdminInvoicePaymentShippingSection.getShippingMethodAndRates}}" stepKey="openShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{AdminInvoicePaymentShippingSection.shippingMethod}}" stepKey="chooseShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethodLoad"/> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <waitForPageLoad stepKey="waitForSuccess"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..a8e982475253f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml @@ -0,0 +1,88 @@ +<?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="StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search configurable product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search configurable product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20389"/> + <group value="ConfigurableProduct"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="categoryHandle" before="simple1Handle"/> + + <createData entity="SimpleProduct" stepKey="simple1Handle" before="simple2Handle"> + <requiredEntity createDataKey="categoryHandle"/> + </createData> + + <createData entity="SimpleProduct" stepKey="simple2Handle" before="product"> + <requiredEntity createDataKey="categoryHandle"/> + </createData> + + <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> + <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> + + <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> + + <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + + <createData entity="SimpleOne" stepKey="childProductHandle1"> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption1Handle"/> + </createData> + <createData entity="SimpleOne" stepKey="childProductHandle2"> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption2Handle"/> + </createData> + + <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption1Handle"/> + <requiredEntity createDataKey="getAttributeOption2Handle"/> + </createData> + + <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="childProductHandle1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="childProductHandle2"/> + </createData> + </before> + <after> + <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml index ac468fc92e4db..805727e29a17a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml @@ -88,7 +88,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index 1075f79aef187..16400fa837b1c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-249"/> <group value="ConfigurableProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> @@ -131,7 +132,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml index 836bc2cdca970..2f5ee036b1420 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml @@ -31,7 +31,11 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify configurable product details in storefront product view --> @@ -72,7 +76,11 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify configurable product options in storefront product view --> @@ -113,7 +121,11 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify adding configurable product to cart after an option is selected in storefront product view --> @@ -151,7 +163,11 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify not able to add configurable product to cart when no option is selected in storefront product view --> @@ -162,4 +178,108 @@ <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> <see userInput="This is a required field" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsError}}" stepKey="seeError"/> </test> + + <test name="StorefrontConfigurableProductVariationsTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Configurable Product"/> + <title value="Customer should get the right options list"/> + <description value="Customer should get the right options list for each variation of configurable product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-23027"/> + <useCaseId value="MC-22732"/> + <group value="configurable_product"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Add first attribute with options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createFirstAttribute"/> + <createData entity="productAttributeOption1" stepKey="createFirstAttributeFirstOption"> + <requiredEntity createDataKey="createFirstAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createFirstAttributeSecondOption"> + <requiredEntity createDataKey="createFirstAttribute"/> + </createData> + <!-- Add second attribute with options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createSecondAttribute"/> + <createData entity="productAttributeOption1" stepKey="createSecondAttributeFirstOption"> + <requiredEntity createDataKey="createSecondAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createSecondAttributeSecondOption"> + <requiredEntity createDataKey="createSecondAttribute"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{BaseConfigurableProduct.sku}}"/> + </actionGroup> + <deleteData createDataKey="createFirstAttribute" stepKey="deleteFirstAttribute"/> + <deleteData createDataKey="createSecondAttribute" stepKey="deleteSecondAttribute"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="navigateToCreateProductPage"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="SetCategoryByName" stepKey="addCategoryToProduct"> + <argument name="categoryName" value="$createCategory.name$"/> + </actionGroup> + <actionGroup ref="SetProductUrlKeyByString" stepKey="fillUrlKey"> + <argument name="urlKey" value="{{BaseConfigurableProduct.urlKey}}"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <actionGroup ref="AdminSelectAttributeInConfigurableAttributesGrid" stepKey="checkFirstAttribute"> + <argument name="attributeCode" value="$createFirstAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="AdminSelectAttributeInConfigurableAttributesGrid" stepKey="checkSecondAttribute"> + <argument name="attributeCode" value="$createSecondAttribute.attribute_code$"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <waitForPageLoad stepKey="waitForStepLoad"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute($createFirstAttribute.default_frontend_label$)}}" stepKey="clickFirstAttributeSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute($createSecondAttribute.default_frontend_label$)}}" stepKey="clickSecondAttributeSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickSecondNextStep"/> + <waitForElement selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitThirdNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickThirdStep"/> + <waitForElement selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitGenerateConfigurationsButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickToGenerateConfigurations"/> + + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.variationsGrid}}" stepKey="waitForVariationsGrid"/> + <actionGroup ref="AdminChangeConfigurableProductVariationQty" stepKey="setFirstVariationQuantity"> + <argument name="rowIndex" value="0"/> + <argument name="quantity" value="0"/> + </actionGroup> + <actionGroup ref="AdminChangeConfigurableProductVariationQty" stepKey="setSecondVariationQuantity"> + <argument name="rowIndex" value="1"/> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="AdminChangeConfigurableProductVariationQty" stepKey="setThirdVariationQuantity"> + <argument name="rowIndex" value="2"/> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="AdminChangeConfigurableProductVariationQty" stepKey="setFourthVariationQuantity"> + <argument name="rowIndex" value="3"/> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="saveConfigurableProduct" stepKey="saveConfigurableProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> + <grabValueFrom selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="grabUrlKey"/> + <amOnPage url="{$grabUrlKey}.html" stepKey="amOnConfigurableProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productOptionSelect($createFirstAttribute.default_frontend_label$)}}" stepKey="waitForFirstSelect"/> + <selectOption userInput="$createFirstAttributeFirstOption.option[store_labels][0][label]$" selector="{{StorefrontProductInfoMainSection.productOptionSelect($createFirstAttribute.default_frontend_label$)}}" stepKey="selectFirstAttributeFirstOption"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productOptionSelect($createSecondAttribute.default_frontend_label$)}}" stepKey="waitForSecondSelect"/> + <selectOption userInput="$createSecondAttributeSecondOption.option[store_labels][0][label]$" selector="{{StorefrontProductInfoMainSection.productOptionSelect($createSecondAttribute.default_frontend_label$)}}" stepKey="selectSecondAttributeSecondOption"/> + </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml index cc8291a83eb40..65e1d3a74f060 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml @@ -22,17 +22,22 @@ <before> <createData entity="ApiCategory" stepKey="createCategory"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <!-- Create a configurable product via the UI --> <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> <argument name="product" value="_defaultProduct"/> <argument name="category" value="$$createCategory$$"/> </actionGroup> + <!-- TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush eav" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup stepKey="deleteProduct" ref="deleteProductBySku"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify the storefront category grid view --> @@ -68,7 +73,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify storefront category list view --> @@ -106,7 +111,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Should be taken to product details page when adding to cart because an option needs to be selected --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml index d890d59858116..4c955f3385643 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -25,7 +25,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index bb69122dc0be9..182c8c069ab23 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -135,6 +135,9 @@ <waitForPageLoad stepKey="waitForPageLoad1"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Open Category in Store Front and select product attribute option from sidebar --> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> <argument name="categoryName" value="$$createCategory.name$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index c5c2368720b98..36fda4ef3245c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -233,9 +233,7 @@ public function cacheKeyProvider(): array public function testGetCacheKeyInfo(array $expected, string $priceCurrency = null, string $customerGroupId = null) { $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->setMethods([ - 'getCurrentCurrency', - ]) + ->setMethods(['getCurrentCurrency']) ->getMockForAbstractClass(); $storeMock->expects($this->any()) ->method('getCode') @@ -270,9 +268,7 @@ public function testGetJsonConfig() $amountMock = $this->getAmountMock($amount); $priceMock = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) - ->setMethods([ - 'getAmount', - ]) + ->setMethods(['getAmount']) ->getMockForAbstractClass(); $priceMock->expects($this->any())->method('getAmount')->willReturn($amountMock); $tierPriceMock = $this->getTierPriceMock($amountMock, $priceQty, $percentage); @@ -287,22 +283,25 @@ public function testGetJsonConfig() ->getMock(); $priceInfoMock->expects($this->any()) ->method('getPrice') - ->willReturnMap([ - ['regular_price', $priceMock], - ['final_price', $priceMock], - ['tier_price', $tierPriceMock], - ]); + ->willReturnMap( + [ + ['regular_price', $priceMock], + ['final_price', $priceMock], + ['tier_price', $tierPriceMock], + ] + ); $productMock->expects($this->any())->method('getTypeInstance')->willReturn($productTypeMock); $productMock->expects($this->any())->method('getPriceInfo')->willReturn($priceInfoMock); $productMock->expects($this->any())->method('isSaleable')->willReturn(true); $productMock->expects($this->any())->method('getId')->willReturn($productId); + $productMock->expects($this->any())->method('getStatus') + ->willReturn(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); $this->helper->expects($this->any()) ->method('getOptions') ->with($productMock, [$productMock]) ->willReturn([]); - $this->product->expects($this->any())->method('getSkipSaleableCheck')->willReturn(true); $attributesData = [ 'attributes' => [], @@ -421,9 +420,7 @@ private function getProductTypeMock(\PHPUnit_Framework_MockObject_MockObject $pr ->willReturn('%s'); $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->setMethods([ - 'getCurrentCurrency', - ]) + ->setMethods(['getCurrentCurrency']) ->getMockForAbstractClass(); $storeMock->expects($this->any()) ->method('getCurrentCurrency') @@ -475,10 +472,7 @@ protected function mockContextObject() protected function getAmountMock($amount): \PHPUnit_Framework_MockObject_MockObject { $amountMock = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) - ->setMethods([ - 'getValue', - 'getBaseAmount', - ]) + ->setMethods(['getValue', 'getBaseAmount']) ->getMockForAbstractClass(); $amountMock->expects($this->any()) ->method('getValue') @@ -506,10 +500,7 @@ protected function getTierPriceMock(\PHPUnit_Framework_MockObject_MockObject $am ]; $tierPriceMock = $this->getMockBuilder(\Magento\Catalog\Pricing\Price\TierPriceInterface::class) - ->setMethods([ - 'getTierPriceList', - 'getSavePercent', - ]) + ->setMethods(['getTierPriceList', 'getSavePercent']) ->getMockForAbstractClass(); $tierPriceMock->expects($this->any()) ->method('getTierPriceList') diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index c351d12fa813d..165e479d99348 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -266,10 +266,12 @@ public function testSave() ->with('_cache_instance_used_product_attribute_ids') ->willReturn(true); $extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods([ - 'getConfigurableProductOptions', - 'getConfigurableProductLinks' - ]) + ->setMethods( + [ + 'getConfigurableProductOptions', + 'getConfigurableProductLinks' + ] + ) ->getMockForAbstractClass(); $this->entityMetadata->expects($this->any()) ->method('getLinkField') @@ -344,25 +346,13 @@ public function testCanUseAttribute() public function testGetUsedProducts() { - $productCollectionItemData = ['array']; + $productCollectionItem = $this->createMock(\Magento\Catalog\Model\Product::class); + $attributeCollection = $this->createMock(Collection::class); + $product = $this->createMock(\Magento\Catalog\Model\Product::class); + $productCollection = $this->createMock(ProductCollection::class); - $productCollectionItem = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - $attributeCollection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - $productCollection = $this->getMockBuilder(ProductCollection::class) - ->disableOriginalConstructor() - ->getMock(); - - $productCollectionItem->expects($this->once())->method('getData')->willReturn($productCollectionItemData); $attributeCollection->expects($this->any())->method('setProductFilter')->willReturnSelf(); $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(5); - $product->expects($this->once())->method('getIdentities')->willReturn(['123']); $product->expects($this->exactly(2)) ->method('hasData') @@ -388,59 +378,10 @@ public function testGetUsedProducts() $productCollection->expects($this->once())->method('setStoreId')->with(5)->willReturn([]); $productCollection->expects($this->once())->method('getItems')->willReturn([$productCollectionItem]); - $this->serializer->expects($this->once()) - ->method('serialize') - ->with([$productCollectionItemData]) - ->willReturn('result'); - $this->productCollectionFactory->expects($this->any())->method('create')->willReturn($productCollection); $this->model->getUsedProducts($product); } - public function testGetUsedProductsWithDataInCache() - { - $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - $childProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $dataKey = '_cache_instance_products'; - $usedProductsData = [['first']]; - $usedProducts = [$childProduct]; - - $product->expects($this->once()) - ->method('hasData') - ->with($dataKey) - ->willReturn(false); - $product->expects($this->once()) - ->method('setData') - ->with($dataKey, $usedProducts); - $product->expects($this->any()) - ->method('getData') - ->willReturnOnConsecutiveCalls(1, $usedProducts); - - $childProduct->expects($this->once()) - ->method('setData') - ->with($usedProductsData[0]); - - $this->productFactory->expects($this->once()) - ->method('create') - ->willReturn($childProduct); - - $this->cache->expects($this->once()) - ->method('load') - ->willReturn($usedProductsData); - - $this->serializer->expects($this->once()) - ->method('unserialize') - ->with($usedProductsData) - ->willReturn($usedProductsData); - - $this->assertEquals($usedProducts, $this->model->getUsedProducts($product)); - } - /** * @param int $productStore * @@ -878,12 +819,12 @@ public function testSetImageFromChildProduct() ->method('getLinkField') ->willReturn('link'); $productMock->expects($this->any())->method('hasData') - ->withConsecutive(['store_id'], ['_cache_instance_products']) - ->willReturnOnConsecutiveCalls(true, true); + ->withConsecutive(['_cache_instance_products']) + ->willReturnOnConsecutiveCalls(true); $productMock->expects($this->any())->method('getData') - ->withConsecutive(['image'], ['image'], ['link'], ['store_id'], ['_cache_instance_products']) - ->willReturnOnConsecutiveCalls('no_selection', 'no_selection', 1, 1, [$childProductMock]); + ->withConsecutive(['image'], ['image'], ['_cache_instance_products']) + ->willReturnOnConsecutiveCalls('no_selection', 'no_selection', [$childProductMock]); $childProductMock->expects($this->any())->method('getData')->with('image')->willReturn('image_data'); $productMock->expects($this->once())->method('setImage')->with('image_data')->willReturnSelf(); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php index 41995be418130..29bca356c1181 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php @@ -25,46 +25,117 @@ protected function setUp() ); } - public function testGetVariations() + /** + * Variations matrix test. + * + * @param array $expectedResult + * @dataProvider variationProvider + */ + public function testGetVariations($expectedResult) { - $result = [ - [ - 130 => [ - 'value' => '3', - 'label' => 'red', - 'price' => ['value_index' => '3', 'pricing_value' => '', 'is_percent' => '0', 'include' => '1',], - ], - ], - [ - 130 => [ - 'value' => '4', - 'label' => 'blue', - 'price' => ['value_index' => '4', 'pricing_value' => '', 'is_percent' => '0', 'include' => '1',], - ], - ], - ]; - $input = [ - 130 => [ - 'values' => [ - [ - 'value_index' => '3', - 'pricing_value' => '', - 'is_percent' => '0', - 'include' => '1' - ], - [ - 'value_index' => '4', - 'pricing_value' => '', - 'is_percent' => '0', - 'include' => '1' + $this->assertEquals($expectedResult['result'], $this->model->getVariations($expectedResult['input'])); + } + + /** + * Test data provider. + */ + public function variationProvider() + { + return [ + [ + 'with_attribute_id' => [ + 'result' => [ + [ + 130 => [ + 'value' => '3', + 'label' => 'red', + 'price' => [ + 'value_index' => '3', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + ], + [ + 130 => [ + 'value' => '4', + 'label' => 'blue', + 'price' => [ + 'value_index' => '4', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + ], ], + 'input' => [ + 130 => [ + 'values' => [ + [ + 'value_index' => '3', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + [ + 'value_index' => '4', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + 'attribute_id' => '130', + 'options' => [ + [ + 'value' => '3', + 'label' => 'red' + ], + ['value' => '4', + 'label' => 'blue' + ] + ], + ], + ] ], - 'attribute_id' => '130', - 'options' => [['value' => '3', 'label' => 'red',], ['value' => '4', 'label' => 'blue',],], - ], + 'without_attribute_id' => [ + 'result' => [ + [ + 130 => [ + 'value' => '4', + 'label' => 'blue', + 'price' => [ + 'value_index' => '4', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + ], + ], + 'input' => [ + 130 => [ + 'values' => [ + [ + 'value_index' => '3', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ] + ], + 'attribute_id' => '', + 'options' => [ + [ + 'value' => '3', + 'label' => 'red' + ] + ], + ], + ] + ] + ] ]; - - $this->assertEquals($result, $this->model->getVariations($input)); } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php index 80979148c4959..bebbb04405c4b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php @@ -176,14 +176,16 @@ private function createProductMock(): \PHPUnit_Framework_MockObject_MockObject { $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() - ->setMethods([ - 'getAttribute', - 'getId', - 'setQuoteItemQty', - 'setQuoteItemPrice', - 'getTypeId', - 'hasData', - ]) + ->setMethods( + [ + 'getAttribute', + 'getId', + 'setQuoteItemQty', + 'setQuoteItemPrice', + 'getTypeId', + 'hasData', + ] + ) ->getMock(); $productMock ->expects($this->any()) @@ -223,4 +225,38 @@ public function testChildIsNotUsedForValidation() $this->validatorPlugin->beforeValidate($this->validator, $item); } + + /** + * Test for Configurable product in invalid state with no children does not raise error + */ + public function testChildIsNotUsedForValidationWhenConfigurableProductIsMissingChildren() + { + $configurableProductMock = $this->createProductMock(); + $configurableProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Configurable::TYPE_CODE); + + $configurableProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(false); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct', 'getChildren']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($configurableProductMock); + $item->expects($this->any()) + ->method('getChildren') + ->willReturn([]); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } } diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php index 7d337c57d7e77..ade56edeb3dfc 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; @@ -16,7 +18,7 @@ class ConfigurableQty extends AbstractModifier const CODE_QTY_CONTAINER = 'quantity_and_stock_status_qty'; /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -24,7 +26,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php index c474acbec5094..ec69baeb92cb9 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php @@ -21,6 +21,8 @@ use Magento\Framework\Escaper; /** + * Associated products helper + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AssociatedProducts @@ -213,6 +215,7 @@ public function getConfigurableAttributesData() 'code' => $attribute['code'], 'label' => $attribute['label'], 'position' => $attribute['position'], + '__disableTmpl' => true ]; foreach ($attribute['chosen'] as $chosenOption) { @@ -231,6 +234,8 @@ public function getConfigurableAttributesData() * * @return void * @throws \Zend_Currency_Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ protected function prepareVariations() { @@ -261,15 +266,18 @@ protected function prepareVariations() 'id' => $attribute->getAttributeId(), 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], 'chosen' => [], + '__disableTmpl' => true ]; - foreach ($attribute->getOptions() as $option) { - if (!empty($option->getValue())) { - $attributes[$attribute->getAttributeId()]['options'][$option->getValue()] = [ + $options = $attribute->usesSource() ? $attribute->getSource()->getAllOptions() : []; + foreach ($options as $option) { + if (!empty($option['value'])) { + $attributes[$attribute->getAttributeId()]['options'][$option['value']] = [ 'attribute_code' => $attribute->getAttributeCode(), 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $option->getValue(), - 'label' => $option->getLabel(), - 'value' => $option->getValue(), + 'id' => $option['value'], + 'label' => $option['label'], + 'value' => $option['value'], + '__disableTmpl' => true ]; } } @@ -281,6 +289,7 @@ protected function prepareVariations() 'id' => $optionId, 'label' => $variation[$attribute->getId()]['label'], 'value' => $optionId, + '__disableTmpl' => true ]; $variationOptions[] = $variationOption; $attributes[$attribute->getAttributeId()]['chosen'][$optionId] = $variationOption; @@ -306,6 +315,7 @@ protected function prepareVariations() 'newProduct' => 0, 'attributes' => $this->getTextAttributes($variationOptions), 'thumbnail_image' => $this->imageHelper->init($product, 'product_thumbnail_image')->getUrl(), + '__disableTmpl' => true ]; $productIds[] = $product->getId(); } @@ -316,6 +326,7 @@ protected function prepareVariations() $this->productIds = $productIds; $this->productAttributes = array_values($attributes); } + //phpcs: enable /** * Get JSON string that contains attribute code and value diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index b8f7ed67a9868..c8a278df92dc6 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -256,4 +256,12 @@ </argument> </arguments> </type> + <type name="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument> + </arguments> + <arguments> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index df96829b354c8..b2d50f54f5334 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -13,4 +13,7 @@ <type name="Magento\Catalog\Model\Product"> <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" /> </type> + <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> + <plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index 1166adca97255..844422b2a2d7a 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,7 +17,7 @@ </legend> <div class="product-options fieldset admin__fieldset"> <?php foreach ($_attributes as $_attribute) : ?> - <div class="field admin__field _required required"> + <div class="field admin__field required"> <label class="label admin__field-label"><?= $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())); ?></label> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml index c11a1adc19896..240c5e65c79c3 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml @@ -64,7 +64,7 @@ "productsProvider": "configurable_associated_product_listing.data_source", "productsMassAction": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns.ids", "productsColumns": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns", - "productsGridUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/associated_grid', ['componentJson' => true]) ?>", + "productsGridUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_associated/grid', ['componentJson' => true]) ?>", "configurableVariations": "configurableVariations" } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index 6c790c634ee93..16dbf9ec23cd6 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -106,6 +106,8 @@ define([ errorOption, allOptions = []; + newOption.label = $.trim(newOption.label); + if (_.isEmpty(newOption.label)) { return false; } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js index 00bf1feff7fb5..eed887037bc96 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js @@ -237,12 +237,21 @@ define([ getImageProperty: function (node) { var types = node.find('[data-role=gallery]').productGallery('option').types, images = _.map(node.find('[data-role=image]'), function (image) { - var imageData = $(image).data('imageData'); + var imageData = $(image).data('imageData'), + positionElement; imageData.galleryTypes = _.pluck(_.filter(types, function (type) { return type.value === imageData.file; }), 'code'); + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + positionElement = + $(image).find('[name="product[media_gallery][images][' + imageData.file_id + '][position]"]'); + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + if (!_.isEmpty(positionElement.val())) { + imageData.position = positionElement.val(); + } + return imageData; }); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index ae564610e4b0b..faacbd03f20b0 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -443,6 +443,10 @@ define([ } for (i = 0; i < options.length; i++) { + if (prevConfig && typeof allowedProductsByOption[i] === 'undefined') { + continue; //jscs:ignore disallowKeywords + } + allowedProducts = prevConfig ? allowedProductsByOption[i] : options[i].products.slice(0); optionPriceDiff = 0; diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php index f1971e228ac05..4a613254ddf84 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\Stdlib\ArrayManager; use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; @@ -76,6 +77,10 @@ public function execute(array $cartItemData): array } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('Could not find specified product.')); } + $configurableProductLinks = $parentProduct->getExtensionAttributes()->getConfigurableProductLinks(); + if (!in_array($product->getId(), $configurableProductLinks)) { + throw new GraphQlInputException(__('Could not find specified product.')); + } $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $this->optionCollection->addProductId((int)$parentProduct->getData($linkField)); $options = $this->optionCollection->getAttributesByProductId((int)$parentProduct->getData($linkField)); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php index 3e07fecb2ebe7..f28bf97adf930 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php @@ -18,6 +18,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\CatalogGraphQl\Model\Resolver\Products\Query\FieldSelection; /** * @inheritdoc @@ -49,25 +50,33 @@ class ConfigurableVariant implements ResolverInterface */ private $metadataPool; + /** + * @var FieldSelection + */ + private $fieldSelection; + /** * @param Collection $variantCollection * @param OptionCollection $optionCollection * @param ValueFactory $valueFactory * @param AttributeCollection $attributeCollection * @param MetadataPool $metadataPool + * @param FieldSelection $fieldSelection */ public function __construct( Collection $variantCollection, OptionCollection $optionCollection, ValueFactory $valueFactory, AttributeCollection $attributeCollection, - MetadataPool $metadataPool + MetadataPool $metadataPool, + FieldSelection $fieldSelection ) { $this->variantCollection = $variantCollection; $this->optionCollection = $optionCollection; $this->valueFactory = $valueFactory; $this->attributeCollection = $attributeCollection; $this->metadataPool = $metadataPool; + $this->fieldSelection = $fieldSelection; } /** @@ -84,9 +93,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } $this->variantCollection->addParentProduct($value['model']); - $fields = $this->getProductFields($info); - $matchedFields = $this->attributeCollection->getRequestAttributes($fields); - $this->variantCollection->addEavAttributes($matchedFields); + $fields = $this->fieldSelection->getProductsFieldSelection($info); + $this->variantCollection->addEavAttributes($fields); $this->optionCollection->addProductId((int)$value[$linkField]); $result = function () use ($value, $linkField) { @@ -103,26 +111,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return $this->valueFactory->create($result); } - - /** - * Return field names for all requested product fields. - * - * @param ResolveInfo $info - * @return string[] - */ - private function getProductFields(ResolveInfo $info) - { - $fieldNames = []; - foreach ($info->fieldNodes as $node) { - if ($node->name->value !== 'product') { - continue; - } - - foreach ($node->selectionSet->selections as $selectionNode) { - $fieldNames[] = $selectionNode->name->value; - } - } - - return $fieldNames; - } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..4dfa09d77cec2 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableRegularPrice; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; + +/** + * Provides product prices for configurable products + */ +class Provider implements ProviderInterface +{ + /** + * @var ConfigurableOptionsProviderInterface + */ + private $optionsProvider; + + /** + * @var array + */ + private $minimumFinalAmounts = []; + + /** + * @var array + */ + private $maximumFinalAmounts = []; + + /** + * @param ConfigurableOptionsProviderInterface $optionsProvider + */ + public function __construct( + ConfigurableOptionsProviderInterface $optionsProvider + ) { + $this->optionsProvider = $optionsProvider; + } + + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + if (!isset($this->minimumFinalAmounts[$product->getId()])) { + $minimumAmount = null; + foreach ($this->optionsProvider->getProducts($product) as $variant) { + $variantAmount = $variant->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getAmount(); + if (!$minimumAmount || ($variantAmount->getValue() < $minimumAmount->getValue())) { + $minimumAmount = $variantAmount; + $this->minimumFinalAmounts[$product->getId()] = $variantAmount; + } + } + } + + return $this->minimumFinalAmounts[$product->getId()]; + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + /** @var ConfigurableRegularPrice $regularPrice */ + $regularPrice = $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE); + return $regularPrice->getMinRegularAmount(); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + if (!isset($this->maximumFinalAmounts[$product->getId()])) { + $maximumAmount = null; + foreach ($this->optionsProvider->getProducts($product) as $variant) { + $variantAmount = $variant->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getAmount(); + if (!$maximumAmount || ($variantAmount->getValue() > $maximumAmount->getValue())) { + $maximumAmount = $variantAmount; + $this->maximumFinalAmounts[$product->getId()] = $variantAmount; + } + } + } + + return $this->maximumFinalAmounts[$product->getId()]; + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + /** @var ConfigurableRegularPrice $regularPrice */ + $regularPrice = $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE); + return $regularPrice->getMaxRegularAmount(); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index dd2b84e1da539..faf666144422c 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -44,7 +44,10 @@ public function resolve( } $data = []; - foreach ($value['options'] as $option) { + foreach ($value['options'] as $optionId => $option) { + if (!isset($option['attribute_code'])) { + continue; + } $code = $option['attribute_code']; /** @var Product|null $model */ $model = $value['product']['model'] ?? null; @@ -52,18 +55,54 @@ public function resolve( continue; } - foreach ($option['values'] as $optionValue) { - if ($optionValue['value_index'] != $model->getData($code)) { - continue; + if (isset($option['options_map'])) { + $optionsFromMap = $this->getOptionsFromMap( + $option['options_map'] ?? [], + $code, + (int) $optionId, + (int) $model->getData($code) + ); + if (!empty($optionsFromMap)) { + $data[] = $optionsFromMap; } - $data[] = [ - 'label' => $optionValue['label'], - 'code' => $code, - 'use_default_value' => $optionValue['use_default_value'], - 'value_index' => $optionValue['value_index'] - ]; } } return $data; } + + /** + * Get options by index mapping + * + * @param array $optionMap + * @param string $code + * @param int $optionId + * @param int $attributeCodeId + * @return array + */ + private function getOptionsFromMap(array $optionMap, string $code, int $optionId, int $attributeCodeId): array + { + $data = []; + if (isset($optionMap[$optionId . ':' . $attributeCodeId])) { + $optionValue = $optionMap[$optionId . ':' . $attributeCodeId]; + $data = $this->getOptionsArray($optionValue, $code); + } + return $data; + } + + /** + * Get options formatted as array + * + * @param array $optionValue + * @param string $code + * @return array + */ + private function getOptionsArray(array $optionValue, string $code): array + { + return [ + 'label' => $optionValue['label'] ?? null, + 'code' => $code, + 'use_default_value' => $optionValue['use_default_value'] ?? null, + 'value_index' => $optionValue['value_index'] ?? null, + ]; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index d517c9aa29bd3..6c4371b23927e 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -14,6 +14,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; /** * Collection for fetching configurable child product data. @@ -55,22 +56,30 @@ class Collection */ private $collectionProcessor; + /** + * @var CollectionPostProcessor + */ + private $collectionPostProcessor; + /** * @param CollectionFactory $childCollectionFactory * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param MetadataPool $metadataPool * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionPostProcessor $collectionPostProcessor */ public function __construct( CollectionFactory $childCollectionFactory, SearchCriteriaBuilder $searchCriteriaBuilder, MetadataPool $metadataPool, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + CollectionPostProcessor $collectionPostProcessor ) { $this->childCollectionFactory = $childCollectionFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->metadataPool = $metadataPool; $this->collectionProcessor = $collectionProcessor; + $this->collectionPostProcessor = $collectionPostProcessor; } /** @@ -126,7 +135,6 @@ public function getChildProductsByParentId(int $id) : array * Fetch all children products from parent id's. * * @return array - * @throws \Exception */ private function fetch() : array { @@ -144,9 +152,11 @@ private function fetch() : array $this->searchCriteriaBuilder->create(), $attributeData ); + $childCollection->load(); + $this->collectionPostProcessor->process($childCollection, $attributeData); /** @var Product $childProduct */ - foreach ($childCollection->getItems() as $childProduct) { + foreach ($childCollection as $childProduct) { $formattedChild = ['model' => $childProduct, 'sku' => $childProduct->getSku()]; $parentId = (int)$childProduct->getParentId(); if (!isset($this->childrenMap[$parentId])) { @@ -168,7 +178,7 @@ private function fetch() : array */ private function getAttributesCodes(Product $currentProduct): array { - $attributeCodes = []; + $attributeCodes = $this->attributeCodes; $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); foreach ($allowAttributes as $attribute) { $productAttribute = $attribute->getProductAttribute(); diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index 1d72524a31b76..f82bb0dbd4d91 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -29,4 +29,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="configurable" xsi:type="object">Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Cookie/Block/DataProviders/SessionConfig.php b/app/code/Magento/Cookie/Block/DataProviders/SessionConfig.php new file mode 100644 index 0000000000000..9d8ae5b19be9c --- /dev/null +++ b/app/code/Magento/Cookie/Block/DataProviders/SessionConfig.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cookie\Block\DataProviders; + +use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Provide cookie configuration + */ +class SessionConfig implements ArgumentInterface +{ + /** + * Session config + * + * @var ConfigInterface + */ + private $sessionConfig; + + /** + * Constructor + * + * @param ConfigInterface $sessionConfig + */ + public function __construct( + ConfigInterface $sessionConfig + ) { + $this->sessionConfig = $sessionConfig; + } + /** + * Get session.cookie_secure + * + * @return bool + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + */ + public function getCookieSecure() + { + return $this->sessionConfig->getCookieSecure(); + } +} diff --git a/app/code/Magento/Cookie/Block/RequireCookie.php b/app/code/Magento/Cookie/Block/RequireCookie.php index 0a836e5441540..a9c2310b2dfc1 100644 --- a/app/code/Magento/Cookie/Block/RequireCookie.php +++ b/app/code/Magento/Cookie/Block/RequireCookie.php @@ -3,15 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - +declare(strict_types=1); /** * Frontend form key content block */ namespace Magento\Cookie\Block; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; + /** + * Block Require Cookie + * * @api * @since 100.0.2 + * + * Class \Magento\Cookie\Block\RequireCookie */ class RequireCookie extends \Magento\Framework\View\Element\Template { @@ -22,9 +29,11 @@ class RequireCookie extends \Magento\Framework\View\Element\Template */ public function getScriptOptions() { + $isRedirectCmsPage = (boolean)$this->_scopeConfig->getValue('web/browser_capabilities/cookies'); $params = [ 'noCookieUrl' => $this->escapeUrl($this->getUrl('cookie/index/noCookies/')), - 'triggers' => $this->escapeHtml($this->getTriggers()) + 'triggers' => $this->escapeHtml($this->getTriggers()), + 'isRedirectCmsPage' => $isRedirectCmsPage ]; return json_encode($params); } diff --git a/app/code/Magento/Cookie/Test/Unit/Block/RequireCookieTest.php b/app/code/Magento/Cookie/Test/Unit/Block/RequireCookieTest.php new file mode 100644 index 0000000000000..5208f0740a610 --- /dev/null +++ b/app/code/Magento/Cookie/Test/Unit/Block/RequireCookieTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cookie\Test\Unit\Block; + +use Magento\Cookie\Block\RequireCookie; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Template\Context; + +/** + * Class \Magento\Cookie\Test\Unit\Block\RequireCookieTest + */ +class RequireCookieTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RequireCookie + */ + private $block; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Context + */ + private $context; + + /** + * Setup Environment + */ + protected function setUp() + { + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMockForAbstractClass(); + $this->context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->any())->method('getScopeConfig') + ->willReturn($this->scopeConfig); + $this->block = $this->getMockBuilder(RequireCookie::class) + ->setMethods(['escapeHtml', 'escapeUrl', 'getUrl', 'getTriggers']) + ->setConstructorArgs( + [ + 'context' => $this->context + ] + )->getMock(); + } + + /** + * Test getScriptOptions() when the settings "Redirect to CMS-page if Cookies are Disabled" is "Yes" + */ + public function testGetScriptOptionsWhenRedirectToCmsIsYes() + { + $this->scopeConfig->expects($this->any())->method('getValue') + ->with('web/browser_capabilities/cookies') + ->willReturn('1'); + + $this->block->expects($this->any())->method('getUrl') + ->with('cookie/index/noCookies/') + ->willReturn('http://magento.com/cookie/index/noCookies/'); + $this->block->expects($this->any())->method('getTriggers') + ->willReturn('test'); + $this->block->expects($this->any())->method('escapeUrl') + ->with('http://magento.com/cookie/index/noCookies/') + ->willReturn('http://magento.com/cookie/index/noCookies/'); + $this->block->expects($this->any())->method('escapeHtml') + ->with('test') + ->willReturn('test'); + + $this->assertEquals( + '{"noCookieUrl":"http:\/\/magento.com\/cookie\/index\/noCookies\/",' . + '"triggers":"test","isRedirectCmsPage":true}', + $this->block->getScriptOptions() + ); + } + + /** + * Test getScriptOptions() when the settings "Redirect to CMS-page if Cookies are Disabled" is "No" + */ + public function testGetScriptOptionsWhenRedirectToCmsIsNo() + { + $this->scopeConfig->expects($this->any())->method('getValue') + ->with('web/browser_capabilities/cookies') + ->willReturn('0'); + + $this->block->expects($this->any())->method('getUrl') + ->with('cookie/index/noCookies/') + ->willReturn('http://magento.com/cookie/index/noCookies/'); + $this->block->expects($this->any())->method('getTriggers') + ->willReturn('test'); + $this->block->expects($this->any())->method('escapeUrl') + ->with('http://magento.com/cookie/index/noCookies/') + ->willReturn('http://magento.com/cookie/index/noCookies/'); + $this->block->expects($this->any())->method('escapeHtml') + ->with('test') + ->willReturn('test'); + + $this->assertEquals( + '{"noCookieUrl":"http:\/\/magento.com\/cookie\/index\/noCookies\/",' . + '"triggers":"test","isRedirectCmsPage":false}', + $this->block->getScriptOptions() + ); + } +} diff --git a/app/code/Magento/Cookie/i18n/en_US.csv b/app/code/Magento/Cookie/i18n/en_US.csv index 09424c22833fe..7fc98c0ad4c58 100644 --- a/app/code/Magento/Cookie/i18n/en_US.csv +++ b/app/code/Magento/Cookie/i18n/en_US.csv @@ -11,3 +11,5 @@ "Cookie Domain","Cookie Domain" "Use HTTP Only","Use HTTP Only" "Cookie Restriction Mode","Cookie Restriction Mode" +"Cookies are disabled in your browser.","Cookies are disabled in your browser." + diff --git a/app/code/Magento/Cookie/view/adminhtml/layout/default.xml b/app/code/Magento/Cookie/view/adminhtml/layout/default.xml new file mode 100644 index 0000000000000..2862cf917856d --- /dev/null +++ b/app/code/Magento/Cookie/view/adminhtml/layout/default.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="after.body.start"> + <block class="Magento\Framework\View\Element\Js\Cookie" name="cookie_config" template="Magento_Cookie::html/cookie.phtml"> + <arguments> + <argument name="session_config" xsi:type="object">Magento\Cookie\Block\DataProviders\SessionConfig</argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/Cookie/view/base/requirejs-config.js b/app/code/Magento/Cookie/view/base/requirejs-config.js new file mode 100644 index 0000000000000..b4362ffd80cb6 --- /dev/null +++ b/app/code/Magento/Cookie/view/base/requirejs-config.js @@ -0,0 +1,10 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + paths: { + 'jquery/jquery-storageapi': 'Magento_Cookie/js/jquery.storageapi.extended' + } +}; diff --git a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml new file mode 100644 index 0000000000000..b05c53db02abf --- /dev/null +++ b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * Cookie settings initialization script + * + * @var $block \Magento\Framework\View\Element\Js\Cookie + */ +?> + +<script> + window.cookiesConfig = window.cookiesConfig || {}; + window.cookiesConfig.secure = <?= /* @noEscape */ $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false' ?>; +</script> diff --git a/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js b/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js new file mode 100644 index 0000000000000..c026b205f0374 --- /dev/null +++ b/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js @@ -0,0 +1,69 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/jquery.cookie', + 'jquery/jquery.storageapi.min' +], function ($) { + 'use strict'; + + /** + * + * @param {Object} storage + * @private + */ + function _extend(storage) { + $.extend(storage, { + _secure: window.cookiesConfig ? window.cookiesConfig.secure : false, + + /** + * Set value under name + * @param {String} name + * @param {String} value + * @param {Object} [options] + */ + setItem: function (name, value, options) { + var _default = { + expires: this._expires, + path: this._path, + domain: this._domain, + secure: this._secure + }; + + $.cookie(this._prefix + name, value, $.extend(_default, options || {})); + }, + + /** + * Set default options + * @param {Object} c + * @returns {storage} + */ + setConf: function (c) { + if (c.path) { + this._path = c.path; + } + + if (c.domain) { + this._domain = c.domain; + } + + if (c.expires) { + this._expires = c.expires; + } + + if (typeof c.secure !== 'undefined') { + this._secure = c.secure; + } + + return this; + } + }); + } + + if (window.cookieStorage) { + _extend(window.cookieStorage); + } +}); diff --git a/app/code/Magento/Cookie/view/frontend/layout/default.xml b/app/code/Magento/Cookie/view/frontend/layout/default.xml index 8b6b86e81c51a..5202c624fefe1 100644 --- a/app/code/Magento/Cookie/view/frontend/layout/default.xml +++ b/app/code/Magento/Cookie/view/frontend/layout/default.xml @@ -9,6 +9,11 @@ <body> <referenceContainer name="after.body.start"> <block class="Magento\Cookie\Block\Html\Notices" name="cookie_notices" template="Magento_Cookie::html/notices.phtml"/> + <block class="Magento\Framework\View\Element\Js\Cookie" name="cookie_config" template="Magento_Cookie::html/cookie.phtml"> + <arguments> + <argument name="session_config" xsi:type="object">Magento\Cookie\Block\DataProviders\SessionConfig</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js b/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js index 0a175136f034e..e82d7f2af29ca 100644 --- a/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js +++ b/app/code/Magento/Cookie/view/frontend/web/js/require-cookie.js @@ -8,15 +8,19 @@ */ define([ 'jquery', - 'jquery-ui-modules/widget' -], function ($) { + 'Magento_Ui/js/modal/alert', + 'jquery-ui-modules/widget', + 'mage/mage', + 'mage/translate' +], function ($, alert) { 'use strict'; $.widget('mage.requireCookie', { options: { event: 'click', noCookieUrl: 'enable-cookies', - triggers: ['.action.login', '.action.submit'] + triggers: ['.action.login', '.action.submit'], + isRedirectCmsPage: true }, /** @@ -49,8 +53,16 @@ define([ if (navigator.cookieEnabled) { return; } + event.preventDefault(); - window.location = this.options.noCookieUrl; + + if (this.options.isRedirectCmsPage) { + window.location = this.options.noCookieUrl; + } else { + alert({ + content: $.mage.__('Cookies are disabled in your browser.') + }); + } } }); diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index 582c7c811b71f..365d110421664 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -97,7 +97,7 @@ public function _construct() public function setCronExpr($expr) { $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); - if (sizeof($e) < 5 || sizeof($e) > 6) { + if (count($e) < 5 || count($e) > 6) { throw new CronException(__('Invalid cron expression: %1', $expr)); } @@ -168,7 +168,7 @@ public function matchCronExpression($expr, $num) // handle modulus if (strpos($expr, '/') !== false) { $e = explode('/', $expr); - if (sizeof($e) !== 2) { + if (count($e) !== 2) { throw new CronException(__('Invalid cron expression, expecting \'match/modulus\': %1', $expr)); } if (!is_numeric($e[1])) { @@ -187,7 +187,7 @@ public function matchCronExpression($expr, $num) } elseif (strpos($expr, '-') !== false) { // handle range $e = explode('-', $expr); - if (sizeof($e) !== 2) { + if (count($e) !== 2) { throw new CronException(__('Invalid cron expression, expecting \'from-to\' structure: %1', $expr)); } diff --git a/app/code/Magento/Cron/etc/adminhtml/system.xml b/app/code/Magento/Cron/etc/adminhtml/system.xml index 95d8d4c8a6966..c8753f1b0b56f 100644 --- a/app/code/Magento/Cron/etc/adminhtml/system.xml +++ b/app/code/Magento/Cron/etc/adminhtml/system.xml @@ -15,21 +15,27 @@ <label>Cron configuration options for group: </label> <field id="schedule_generate_every" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Generate Schedules Every</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="schedule_ahead_for" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Schedule Ahead for</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="schedule_lifetime" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Missed if Not Run Within</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="history_cleanup_every" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>History Cleanup Every</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="history_success_lifetime" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Success History Lifetime</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="history_failure_lifetime" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Failure History Lifetime</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="use_separate_process" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Use Separate Process</label> diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index b3061eefa6313..206b8f64f3ae7 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="cron_schedule" resource="default" engine="innodb" comment="Cron Schedule"> <column xsi:type="int" name="schedule_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Schedule Id"/> + comment="Schedule ID"/> <column xsi:type="varchar" name="job_code" nullable="false" length="255" default="0" comment="Job Code"/> <column xsi:type="varchar" name="status" nullable="false" length="7" default="pending" comment="Status"/> <column xsi:type="text" name="messages" nullable="true" comment="Messages"/> diff --git a/app/code/Magento/Csp/Api/CspRendererInterface.php b/app/code/Magento/Csp/Api/CspRendererInterface.php new file mode 100644 index 0000000000000..696d2e147f054 --- /dev/null +++ b/app/code/Magento/Csp/Api/CspRendererInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Framework\App\Response\HttpInterface as HttpResponse; + +/** + * Renders configured CSPs + */ +interface CspRendererInterface +{ + /** + * Render configured CSP for the given HTTP response. + * + * @param HttpResponse $response + * @return void + */ + public function render(HttpResponse $response): void; +} diff --git a/app/code/Magento/Csp/Api/Data/ModeConfiguredInterface.php b/app/code/Magento/Csp/Api/Data/ModeConfiguredInterface.php new file mode 100644 index 0000000000000..5779bbd052792 --- /dev/null +++ b/app/code/Magento/Csp/Api/Data/ModeConfiguredInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api\Data; + +/** + * CSP mode. + */ +interface ModeConfiguredInterface +{ + /** + * Report only mode flag. + * + * In "report-only" mode browsers only report violation but do not restrict them. + * + * @return bool + */ + public function isReportOnly(): bool; + + /** + * URI of endpoint logging reported violations. + * + * Even in "restrict" mode violations can be logged. + * + * @return string|null + */ + public function getReportUri(): ?string; +} diff --git a/app/code/Magento/Csp/Api/Data/PolicyInterface.php b/app/code/Magento/Csp/Api/Data/PolicyInterface.php new file mode 100644 index 0000000000000..c713c4b61417f --- /dev/null +++ b/app/code/Magento/Csp/Api/Data/PolicyInterface.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api\Data; + +/** + * Defined Content Security Policy. + * + * Different policies will have different types of data but they all will have identifiers and string representations. + */ +interface PolicyInterface +{ + /** + * Policy unique name (ID). + * + * @return string + */ + public function getId(): string; + + /** + * Value of the policy. + * + * @return string + */ + public function getValue(): string; +} diff --git a/app/code/Magento/Csp/Api/ModeConfigManagerInterface.php b/app/code/Magento/Csp/Api/ModeConfigManagerInterface.php new file mode 100644 index 0000000000000..79aff996bbf5c --- /dev/null +++ b/app/code/Magento/Csp/Api/ModeConfigManagerInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Csp\Api\Data\ModeConfiguredInterface; + +/** + * CSP mode config manager. + * + * Responsible for CSP mode configurations like report-only/restrict modes, report URL etc. + */ +interface ModeConfigManagerInterface +{ + /** + * Load CSP mode config. + * + * @return ModeConfiguredInterface + * @throws \RuntimeException When failed to retrieve configurations. + */ + public function getConfigured(): ModeConfiguredInterface; +} diff --git a/app/code/Magento/Csp/Api/PolicyCollectorInterface.php b/app/code/Magento/Csp/Api/PolicyCollectorInterface.php new file mode 100644 index 0000000000000..139dec77e040b --- /dev/null +++ b/app/code/Magento/Csp/Api/PolicyCollectorInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Collects CSPs from a source. + */ +interface PolicyCollectorInterface +{ + /** + * Collect all configured policies. + * + * Collector finds CSPs from configurations and returns a list. + * The resulting list will be used to render policies as is so it is a collector's responsibility to include + * previously found policies from $defaultPolicies or redefine them. + * + * @param PolicyInterface[] $defaultPolicies Default policies/policies found previously. + * @return PolicyInterface[] + */ + public function collect(array $defaultPolicies = []): array; +} diff --git a/app/code/Magento/Csp/Api/PolicyRendererInterface.php b/app/code/Magento/Csp/Api/PolicyRendererInterface.php new file mode 100644 index 0000000000000..db761598291d4 --- /dev/null +++ b/app/code/Magento/Csp/Api/PolicyRendererInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; + +/** + * Renders one policy at a time. + * + * Different type of CSPs may require specific renderers due to being represented by different headers. + */ +interface PolicyRendererInterface +{ + /** + * Render a policy for a response. + * + * @param PolicyInterface $policy + * @param HttpResponse $response + * @return void + */ + public function render(PolicyInterface $policy, HttpResponse $response): void; + + /** + * Would this renderer work for given policy? + * + * @param PolicyInterface $policy + * @return bool + */ + public function canRender(PolicyInterface $policy): bool; +} diff --git a/app/code/Magento/Csp/LICENSE.txt b/app/code/Magento/Csp/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Csp/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/Csp/LICENSE_AFL.txt b/app/code/Magento/Csp/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Csp/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/Csp/Model/Collector/Config/FetchPolicyReader.php b/app/code/Magento/Csp/Model/Collector/Config/FetchPolicyReader.php new file mode 100644 index 0000000000000..8699d9588b909 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/Config/FetchPolicyReader.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\Config; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\FetchPolicy; + +/** + * Reads fetch directives. + */ +class FetchPolicyReader implements PolicyReaderInterface +{ + /** + * @inheritDoc + */ + public function read(string $id, $value): PolicyInterface + { + return new FetchPolicy( + $id, + !empty($value['none']), + !empty($value['hosts']) ? array_values($value['hosts']) : [], + !empty($value['schemes']) ? array_values($value['schemes']) : [], + !empty($value['self']), + !empty($value['inline']), + !empty($value['eval']), + [], + [], + !empty($value['dynamic']), + !empty($value['event_handlers']) + ); + } + + /** + * @inheritDoc + */ + public function canRead(string $id): bool + { + return in_array($id, FetchPolicy::POLICIES, true); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/Config/FlagPolicyReader.php b/app/code/Magento/Csp/Model/Collector/Config/FlagPolicyReader.php new file mode 100644 index 0000000000000..f8d1ea962308f --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/Config/FlagPolicyReader.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\Config; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\FlagPolicy; + +/** + * @inheritDoc + */ +class FlagPolicyReader implements PolicyReaderInterface +{ + /** + * @inheritDoc + */ + public function read(string $id, $value): PolicyInterface + { + return new FlagPolicy($id); + } + + /** + * @inheritDoc + */ + public function canRead(string $id): bool + { + return in_array($id, FlagPolicy::POLICIES, true); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/Config/PluginTypesPolicyReader.php b/app/code/Magento/Csp/Model/Collector/Config/PluginTypesPolicyReader.php new file mode 100644 index 0000000000000..7214e6da62273 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/Config/PluginTypesPolicyReader.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\Config; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\PluginTypesPolicy; + +/** + * @inheritDoc + */ +class PluginTypesPolicyReader implements PolicyReaderInterface +{ + /** + * @inheritDoc + */ + public function read(string $id, $value): PolicyInterface + { + return new PluginTypesPolicy(array_values($value['types'])); + } + + /** + * @inheritDoc + */ + public function canRead(string $id): bool + { + return $id === 'plugin-types'; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/Config/PolicyReaderInterface.php b/app/code/Magento/Csp/Model/Collector/Config/PolicyReaderInterface.php new file mode 100644 index 0000000000000..2ebac992222d9 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/Config/PolicyReaderInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\Config; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Initiates a policy DTO based on a value found in Magento config. + */ +interface PolicyReaderInterface +{ + /** + * Read a policy from a config value. + * + * @param string $id + * @param string|array|bool $value + * @return PolicyInterface + */ + public function read(string $id, $value): PolicyInterface; + + /** + * Can given policy be read by this reader? + * + * @param string $id + * @return bool + */ + public function canRead(string $id): bool; +} diff --git a/app/code/Magento/Csp/Model/Collector/Config/PolicyReaderPool.php b/app/code/Magento/Csp/Model/Collector/Config/PolicyReaderPool.php new file mode 100644 index 0000000000000..0fc6fe03e05fa --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/Config/PolicyReaderPool.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\Config; + +/** + * Pool of readers. + */ +class PolicyReaderPool +{ + /** + * @var PolicyReaderInterface[] + */ + private $readers; + + /** + * @param PolicyReaderInterface[] $readers + */ + public function __construct(array $readers) + { + $this->readers = $readers; + } + + /** + * Find a reader for the policy. + * + * @param string $id + * @return PolicyReaderInterface + * @throws \RuntimeException When failed to find a reader for given policy. + */ + public function getReader(string $id): PolicyReaderInterface + { + foreach ($this->readers as $reader) { + if ($reader->canRead($id)) { + return $reader; + } + } + + throw new \RuntimeException(sprintf('Failed to find a config reader for policy #%s', $id)); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/Config/SandboxPolicyReader.php b/app/code/Magento/Csp/Model/Collector/Config/SandboxPolicyReader.php new file mode 100644 index 0000000000000..2699b8d57c037 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/Config/SandboxPolicyReader.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\Config; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\SandboxPolicy; + +/** + * @inheritDoc + */ +class SandboxPolicyReader implements PolicyReaderInterface +{ + /** + * @inheritDoc + */ + public function read(string $id, $value): PolicyInterface + { + return new SandboxPolicy( + !empty($value['forms']), + !empty($value['modals']), + !empty($value['orientation']), + !empty($value['pointer']), + !empty($value['popup']), + !empty($value['popups_to_escape']), + !empty($value['presentation']), + !empty($value['same_origin']), + !empty($value['scripts']), + !empty($value['navigation']), + !empty($value['navigation_by_user']) + ); + } + + /** + * @inheritDoc + */ + public function canRead(string $id): bool + { + return $id === 'sandbox'; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/ConfigCollector.php b/app/code/Magento/Csp/Model/Collector/ConfigCollector.php new file mode 100644 index 0000000000000..34711fe5d8a22 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/ConfigCollector.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\PolicyCollectorInterface; +use Magento\Csp\Model\Collector\Config\PolicyReaderPool; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\State; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Reads Magento config. + */ +class ConfigCollector implements PolicyCollectorInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var PolicyReaderPool + */ + private $readersPool; + + /** + * @var State + */ + private $state; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param ScopeConfigInterface $config + * @param PolicyReaderPool $readersPool + * @param State $state + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ScopeConfigInterface $config, + PolicyReaderPool $readersPool, + State $state, + StoreManagerInterface $storeManager + ) { + $this->config = $config; + $this->readersPool = $readersPool; + $this->state = $state; + $this->storeManager = $storeManager; + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + $collected = $defaultPolicies; + + $configArea = null; + $area = $this->state->getAreaCode(); + if ($area === Area::AREA_ADMINHTML) { + $configArea = 'admin'; + } elseif ($area === Area::AREA_FRONTEND) { + $configArea = 'storefront'; + } + + if ($configArea) { + $policiesConfig = $this->config->getValue( + 'csp/policies/' . $configArea, + ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore() + ); + if (is_array($policiesConfig) && $policiesConfig) { + foreach ($policiesConfig as $policyConfig) { + $collected[] = $this->readersPool->getReader($policyConfig['policy_id']) + ->read($policyConfig['policy_id'], $policyConfig); + } + } + } + + return $collected; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php new file mode 100644 index 0000000000000..ab1ee8bb0befe --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\CspWhitelistXml; + +use Magento\Framework\Config\ConverterInterface; + +/** + * Converts csp_whitelist.xml files' content into config data. + */ +class Converter implements ConverterInterface +{ + /** + * @inheritDoc + */ + public function convert($source) + { + $policyConfig = []; + + /** @var \DOMNodeList $policies */ + $policies = $source->getElementsByTagName('policy'); + /** @var \DOMElement $policy */ + foreach ($policies as $policy) { + if ($policy->nodeType != XML_ELEMENT_NODE) { + continue; + } + $id = $policy->attributes->getNamedItem('id')->nodeValue; + if (!array_key_exists($id, $policyConfig)) { + $policyConfig[$id] = ['hosts' => [], 'hashes' => []]; + } + /** @var \DOMElement $value */ + foreach ($policy->getElementsByTagName('value') as $value) { + if ($value->attributes->getNamedItem('type')->nodeValue === 'host') { + $policyConfig[$id]['hosts'][] = $value->nodeValue; + } else { + $policyConfig[$id]['hashes'][$value->nodeValue] + = $value->attributes->getNamedItem('algorithm')->nodeValue; + } + } + $policyConfig[$id]['hosts'] = array_unique($policyConfig[$id]['hosts']); + $policyConfig[$id]['hashes'] = array_unique($policyConfig[$id]['hashes']); + } + + return $policyConfig; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Reader.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Reader.php new file mode 100644 index 0000000000000..63f570ceb7a71 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Reader.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\CspWhitelistXml; + +use Magento\Framework\Config\Reader\Filesystem; + +/** + * Config reader for csp_whitelist.xml files. + */ +class Reader extends Filesystem +{ + /** + * List of id attributes for merge + * + * @var array + */ + protected $_idAttributes = [ + '/csp_whitelist/policies/policy' => ['id'], + '/csp_whitelist/policies/policy/values/value' => ['id'] + ]; +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/SchemaLocator.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/SchemaLocator.php new file mode 100644 index 0000000000000..285d37a1b6270 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/SchemaLocator.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\CspWhitelistXml; + +use Magento\Framework\Module\Dir; +use Magento\Framework\Config\SchemaLocatorInterface; +use Magento\Framework\Module\Dir\Reader; + +/** + * CSP whitelist config schema locator. + */ +class SchemaLocator implements SchemaLocatorInterface +{ + /** + * Path to corresponding XSD file with validation rules for merged config and per file config. + * + * @var string + */ + private $schema ; + + /** + * @param Reader $moduleReader + */ + public function __construct(Reader $moduleReader) + { + $this->schema = $moduleReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Csp') + . '/csp_whitelist.xsd'; + } + + /** + * @inheritDoc + */ + public function getSchema() + { + return $this->schema; + } + + /** + * @inheritDoc + */ + public function getPerFileSchema() + { + return $this->schema; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php new file mode 100644 index 0000000000000..9f19a5299c063 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\PolicyCollectorInterface; +use Magento\Csp\Model\Collector\CspWhitelistXml\Reader as ConfigReader; +use Magento\Csp\Model\Policy\FetchPolicy; + +/** + * Collects policies defined in csp_whitelist.xml configs. + */ +class CspWhitelistXmlCollector implements PolicyCollectorInterface +{ + /** + * @var ConfigReader + */ + private $configReader; + + /** + * @param ConfigReader $configReader + */ + public function __construct(ConfigReader $configReader) + { + $this->configReader = $configReader; + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + $policies = $defaultPolicies; + $config = $this->configReader->read(); + foreach ($config as $policyId => $values) { + $policies[] = new FetchPolicy( + $policyId, + false, + $values['hosts'], + [], + false, + false, + false, + [], + $values['hashes'], + false, + false + ); + } + + return $policies; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php b/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php new file mode 100644 index 0000000000000..2a8f6c278b078 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\FetchPolicy; + +/** + * @inheritDoc + */ +class FetchPolicyMerger implements MergerInterface +{ + /** + * @inheritDoc + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + /** @var FetchPolicy $policy1 */ + /** @var FetchPolicy $policy2 */ + return new FetchPolicy( + $policy1->getId(), + $policy1->isNoneAllowed() || $policy2->isNoneAllowed(), + array_unique(array_merge($policy1->getHostSources(), $policy2->getHostSources())), + array_unique(array_merge($policy1->getSchemeSources(), $policy2->getSchemeSources())), + $policy1->isSelfAllowed() || $policy2->isSelfAllowed(), + $policy1->isInlineAllowed() || $policy2->isInlineAllowed(), + $policy1->isEvalAllowed() || $policy2->isEvalAllowed(), + array_unique(array_merge($policy1->getNonceValues(), $policy2->getNonceValues())), + array_merge($policy1->getHashes(), $policy2->getHashes()), + $policy1->isDynamicAllowed() || $policy2->isDynamicAllowed(), + $policy1->areEventHandlersAllowed() || $policy2->areEventHandlersAllowed() + ); + } + + /** + * @inheritDoc + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool + { + return ($policy1 instanceof FetchPolicy) && ($policy2 instanceof FetchPolicy); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/FlagPolicyMerger.php b/app/code/Magento/Csp/Model/Collector/FlagPolicyMerger.php new file mode 100644 index 0000000000000..a734feeab1281 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/FlagPolicyMerger.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\FlagPolicy; + +/** + * @inheritDoc + */ +class FlagPolicyMerger implements MergerInterface +{ + /** + * @inheritDoc + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + return $policy1; + } + + /** + * @inheritDoc + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool + { + return ($policy1 instanceof FlagPolicy) && ($policy2 instanceof FlagPolicy); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/MergerInterface.php b/app/code/Magento/Csp/Model/Collector/MergerInterface.php new file mode 100644 index 0000000000000..4a8d78e7b8f4b --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/MergerInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Merges policies with the same ID in order to have only 1 policy DTO-per-policy. + */ +interface MergerInterface +{ + /** + * Merges 2 found policies into 1. + * + * @param PolicyInterface $policy1 + * @param PolicyInterface $policy2 + * @return PolicyInterface + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface; + + /** + * Whether current merger can merge given 2 policies. + * + * @param PolicyInterface $policy1 + * @param PolicyInterface $policy2 + * @return bool + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool; +} diff --git a/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php b/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php new file mode 100644 index 0000000000000..58f2128657788 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\PluginTypesPolicy; + +/** + * @inheritDoc + */ +class PluginTypesPolicyMerger implements MergerInterface +{ + /** + * @inheritDoc + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + /** @var PluginTypesPolicy $policy1 */ + /** @var PluginTypesPolicy $policy2 */ + return new PluginTypesPolicy(array_unique(array_merge($policy1->getTypes(), $policy2->getTypes()))); + } + + /** + * @inheritDoc + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool + { + return ($policy1 instanceof PluginTypesPolicy) && ($policy2 instanceof PluginTypesPolicy); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/SandboxPolicyMerger.php b/app/code/Magento/Csp/Model/Collector/SandboxPolicyMerger.php new file mode 100644 index 0000000000000..3e3f05b1bc845 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/SandboxPolicyMerger.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Policy\SandboxPolicy; + +/** + * @inheritDoc + */ +class SandboxPolicyMerger implements MergerInterface +{ + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + /** @var SandboxPolicy $policy1 */ + /** @var SandboxPolicy $policy2 */ + return new SandboxPolicy( + $policy1->isFormAllowed() || $policy2->isFormAllowed(), + $policy1->isModalsAllowed() || $policy2->isModalsAllowed(), + $policy1->isOrientationLockAllowed() || $policy2->isOrientationLockAllowed(), + $policy1->isPointerLockAllowed() || $policy2->isPointerLockAllowed(), + $policy1->isPopupsAllowed() || $policy2->isPopupsAllowed(), + $policy1->isPopupsToEscapeSandboxAllowed() || $policy2->isPopupsToEscapeSandboxAllowed(), + $policy1->isPresentationAllowed() || $policy2->isPresentationAllowed(), + $policy1->isSameOriginAllowed() || $policy2->isSameOriginAllowed(), + $policy1->isScriptsAllowed() || $policy2->isScriptsAllowed(), + $policy1->isTopNavigationAllowed() || $policy2->isTopNavigationAllowed(), + $policy1->isTopNavigationByUserActivationAllowed() || $policy2->isTopNavigationByUserActivationAllowed() + ); + } + + /** + * @inheritDoc + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool + { + return ($policy1 instanceof SandboxPolicy) && ($policy2 instanceof SandboxPolicy); + } +} diff --git a/app/code/Magento/Csp/Model/CompositePolicyCollector.php b/app/code/Magento/Csp/Model/CompositePolicyCollector.php new file mode 100644 index 0000000000000..b775c91b4e1ef --- /dev/null +++ b/app/code/Magento/Csp/Model/CompositePolicyCollector.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Api\PolicyCollectorInterface; +use Magento\Csp\Model\Collector\MergerInterface; + +/** + * Delegates collecting to multiple collectors. + */ +class CompositePolicyCollector implements PolicyCollectorInterface +{ + /** + * @var PolicyCollectorInterface[] + */ + private $collectors; + + /** + * @var MergerInterface[] + */ + private $mergers; + + /** + * @param PolicyCollectorInterface[] $collectors + * @param MergerInterface[] $mergers + */ + public function __construct(array $collectors, array $mergers) + { + $this->collectors = $collectors; + $this->mergers = $mergers; + } + + /** + * Merge 2 policies with the same ID. + * + * @param PolicyInterface $policy1 + * @param PolicyInterface $policy2 + * @return PolicyInterface + * @throws \RuntimeException When failed to merge. + */ + private function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($policy1, $policy2)) { + return $merger->merge($policy1, $policy2); + } + } + + throw new \RuntimeException(sprintf('Merge for policies #%s was not found', $policy1->getId())); + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + $collected = $defaultPolicies; + foreach ($this->collectors as $collector) { + $collected = $collector->collect($collected); + } + //Merging policies. + /** @var PolicyInterface[] $result */ + $result = []; + foreach ($collected as $policy) { + if (array_key_exists($policy->getId(), $result)) { + $result[$policy->getId()] = $this->merge($result[$policy->getId()], $policy); + } else { + $result[$policy->getId()] = $policy; + } + } + + return array_values($result); + } +} diff --git a/app/code/Magento/Csp/Model/CspRenderer.php b/app/code/Magento/Csp/Model/CspRenderer.php new file mode 100644 index 0000000000000..a883820f6743a --- /dev/null +++ b/app/code/Magento/Csp/Model/CspRenderer.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model; + +use Magento\Csp\Api\CspRendererInterface; +use Magento\Csp\Api\PolicyCollectorInterface; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; + +/** + * @inheritDoc + */ +class CspRenderer implements CspRendererInterface +{ + /** + * @var PolicyRendererPool + */ + private $rendererPool; + + /** + * @var PolicyCollectorInterface + */ + private $collector; + + /** + * @param PolicyRendererPool $rendererPool + * @param PolicyCollectorInterface $collector + */ + public function __construct(PolicyRendererPool $rendererPool, PolicyCollectorInterface $collector) + { + $this->rendererPool = $rendererPool; + $this->collector = $collector; + } + + /** + * @inheritDoc + */ + public function render(HttpResponse $response): void + { + $policies = $this->collector->collect(); + foreach ($policies as $policy) { + $this->rendererPool->getRenderer($policy)->render($policy, $response); + } + } +} diff --git a/app/code/Magento/Csp/Model/Mode/ConfigManager.php b/app/code/Magento/Csp/Model/Mode/ConfigManager.php new file mode 100644 index 0000000000000..9f10154604d5f --- /dev/null +++ b/app/code/Magento/Csp/Model/Mode/ConfigManager.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Mode; + +use Magento\Csp\Api\Data\ModeConfiguredInterface; +use Magento\Csp\Api\ModeConfigManagerInterface; +use Magento\Csp\Model\Mode\Data\ModeConfigured; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\State; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; + +/** + * @inheritDoc + */ +class ConfigManager implements ModeConfigManagerInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var Store + */ + private $storeModel; + + /** + * @var State + */ + private $state; + + /** + * @param ScopeConfigInterface $config + * @param Store $store + * @param State $state + */ + public function __construct(ScopeConfigInterface $config, Store $store, State $state) + { + $this->config = $config; + $this->storeModel = $store; + $this->state = $state; + } + + /** + * @inheritDoc + */ + public function getConfigured(): ModeConfiguredInterface + { + $area = $this->state->getAreaCode(); + if ($area === Area::AREA_ADMINHTML) { + $configArea = 'admin'; + } elseif ($area === Area::AREA_FRONTEND) { + $configArea = 'storefront'; + } else { + throw new \RuntimeException('CSP can only be configured for storefront or admin area'); + } + + $reportOnly = $this->config->isSetFlag( + 'csp/mode/' . $configArea .'/report_only', + ScopeInterface::SCOPE_STORE, + $this->storeModel->getStore() + ); + $reportUri = $this->config->getValue( + 'csp/mode/' . $configArea .'/report_uri', + ScopeInterface::SCOPE_STORE, + $this->storeModel->getStore() + ); + + return new ModeConfigured($reportOnly, !empty($reportUri) ? $reportUri : null); + } +} diff --git a/app/code/Magento/Csp/Model/Mode/Data/ModeConfigured.php b/app/code/Magento/Csp/Model/Mode/Data/ModeConfigured.php new file mode 100644 index 0000000000000..db5e362f560a7 --- /dev/null +++ b/app/code/Magento/Csp/Model/Mode/Data/ModeConfigured.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Mode\Data; + +use Magento\Csp\Api\Data\ModeConfiguredInterface; + +/** + * @inheritDoc + */ +class ModeConfigured implements ModeConfiguredInterface +{ + /** + * @var bool + */ + private $reportOnly; + + /** + * @var string|null + */ + private $reportUri; + + /** + * @param bool $reportOnly + * @param string|null $reportUri + */ + public function __construct(bool $reportOnly, ?string $reportUri) + { + $this->reportOnly = $reportOnly; + $this->reportUri = $reportUri; + } + + /** + * @inheritDoc + */ + public function isReportOnly(): bool + { + return $this->reportOnly; + } + + /** + * @inheritDoc + */ + public function getReportUri(): ?string + { + return $this->reportUri; + } +} diff --git a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php new file mode 100644 index 0000000000000..7350cbe80aecb --- /dev/null +++ b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php @@ -0,0 +1,283 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Policy; + +/** + * Represents a fetch directive. + */ +class FetchPolicy implements SimplePolicyInterface +{ + /** + * List of possible fetch directives. + */ + public const POLICIES = [ + 'default-src', + 'child-src', + 'connect-src', + 'font-src', + 'frame-src', + 'img-src', + 'manifest-src', + 'media-src', + 'object-src', + 'script-src', + 'style-src', + 'base-uri', + 'form-action', + 'frame-ancestors' + ]; + + /** + * @var string + */ + private $id; + + /** + * @var string[] + */ + private $hostSources; + + /** + * @var string[] + */ + private $schemeSources; + + /** + * @var bool + */ + private $selfAllowed; + + /** + * @var bool + */ + private $inlineAllowed; + + /** + * @var bool + */ + private $evalAllowed; + + /** + * @var bool + */ + private $noneAllowed; + + /** + * @var string[] + */ + private $nonceValues; + + /** + * @var string[] + */ + private $hashes; + + /** + * @var bool + */ + private $dynamicAllowed; + + /** + * @var bool + */ + private $eventHandlersAllowed; + + /** + * @param string $id + * @param bool $noneAllowed + * @param string[] $hostSources + * @param string[] $schemeSources + * @param bool $selfAllowed + * @param bool $inlineAllowed + * @param bool $evalAllowed + * @param string[] $nonceValues + * @param string[] $hashValues + * @param bool $dynamicAllowed + * @param bool $eventHandlersAllowed + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $id, + bool $noneAllowed = true, + array $hostSources = [], + array $schemeSources = [], + bool $selfAllowed = false, + bool $inlineAllowed = false, + bool $evalAllowed = false, + array $nonceValues = [], + array $hashValues = [], + bool $dynamicAllowed = false, + bool $eventHandlersAllowed = false + ) { + $this->id = $id; + $this->noneAllowed = $noneAllowed; + $this->hostSources = array_unique($hostSources); + $this->schemeSources = array_unique($schemeSources); + $this->selfAllowed = $selfAllowed; + $this->inlineAllowed = $inlineAllowed; + $this->evalAllowed = $evalAllowed; + $this->nonceValues = array_unique($nonceValues); + $this->hashes = $hashValues; + $this->dynamicAllowed = $dynamicAllowed; + $this->eventHandlersAllowed = $eventHandlersAllowed; + } + + /** + * @inheritDoc + */ + public function getId(): string + { + return $this->id; + } + + /** + * Items can be loaded from given hosts. + * + * @return string[] + */ + public function getHostSources(): array + { + return $this->hostSources; + } + + /** + * Items can be loaded using following schemes. + * + * @return string[] + */ + public function getSchemeSources(): array + { + return $this->schemeSources; + } + + /** + * Items can be loaded from the same host/port as the HTML page. + * + * @return bool + */ + public function isSelfAllowed(): bool + { + return $this->selfAllowed; + } + + /** + * Items can be loaded from tags present on the original HTML page. + * + * @return bool + */ + public function isInlineAllowed(): bool + { + return $this->inlineAllowed; + } + + /** + * Allows creating items from strings. + * + * For example using "eval()" for JavaScript. + * + * @return bool + */ + public function isEvalAllowed(): bool + { + return $this->evalAllowed; + } + + /** + * Content type governed by this policy is disabled completely. + * + * @return bool + */ + public function isNoneAllowed(): bool + { + return $this->noneAllowed; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function getValue(): string + { + if ($this->isNoneAllowed()) { + return '\'none\''; + } else { + $sources = $this->getHostSources(); + foreach ($this->getSchemeSources() as $schemeSource) { + $sources[] = $schemeSource .':'; + } + if ($this->isSelfAllowed()) { + $sources[] = '\'self\''; + } + if ($this->isInlineAllowed()) { + $sources[] = '\'unsafe-inline\''; + } + if ($this->isEvalAllowed()) { + $sources[] = '\'unsafe-eval\''; + } + if ($this->isDynamicAllowed()) { + $sources[] = '\'strict-dynamic\''; + } + if ($this->areEventHandlersAllowed()) { + $sources[] = '\'unsafe-hashes\''; + } + foreach ($this->getNonceValues() as $nonce) { + $sources[] = '\'nonce-' .base64_encode($nonce) .'\''; + } + foreach ($this->getHashes() as $hash => $algorithm) { + $sources[]= "'$algorithm-$hash'"; + } + + return implode(' ', $sources); + } + } + + /** + * Unique cryptographically random numbers marking inline items as trusted. + * + * Contains only numbers, not encoded. + * + * @return string[] + */ + public function getNonceValues(): array + { + return $this->nonceValues; + } + + /** + * Unique hashes generated based on inline items marking them as trusted. + * + * Contains only hashes themselves, encoded into base64. Keys are the hashes, values are algorithms used. + * + * @return string[] + */ + public function getHashes(): array + { + return $this->hashes; + } + + /** + * Is trust to inline items propagated to items loaded by root items. + * + * @return bool + */ + public function isDynamicAllowed(): bool + { + return $this->dynamicAllowed; + } + + /** + * Allows to whitelist event handlers (but not javascript: URLs) with hashes. + * + * @return bool + */ + public function areEventHandlersAllowed(): bool + { + return $this->eventHandlersAllowed; + } +} diff --git a/app/code/Magento/Csp/Model/Policy/FlagPolicy.php b/app/code/Magento/Csp/Model/Policy/FlagPolicy.php new file mode 100644 index 0000000000000..041e1ca5d6229 --- /dev/null +++ b/app/code/Magento/Csp/Model/Policy/FlagPolicy.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Policy; + +/** + * Policies that are used as flags without a value. + */ +class FlagPolicy implements SimplePolicyInterface +{ + public const POLICIES = [ + 'upgrade-insecure-requests', + 'block-all-mixed-content' + ]; + + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * @inheritDoc + */ + public function getId(): string + { + return $this->id; + } + + /** + * @inheritDoc + */ + public function getValue(): string + { + return ''; + } +} diff --git a/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php b/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php new file mode 100644 index 0000000000000..4f34f49bfffe7 --- /dev/null +++ b/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Policy; + +/** + * Governs allowed plugin mime-types. + */ +class PluginTypesPolicy implements SimplePolicyInterface +{ + /** + * @var string[] + */ + private $types; + + /** + * @param string[] $types + */ + public function __construct(array $types) + { + if (!$types) { + throw new \RuntimeException('PluginTypePolicy must be given at least 1 type.'); + } + $this->types = array_unique($types); + } + + /** + * @inheritDoc + */ + public function getId(): string + { + return 'plugin-types'; + } + + /** + * @inheritDoc + */ + public function getValue(): string + { + return implode(' ', $this->getTypes()); + } + + /** + * Mime types of allowed plugins. + * + * Types like "application/x-shockwave-flash", "application/x-java-applet". + * Will only work if object-src directive != "none". + * + * @return string[] + */ + public function getTypes(): array + { + return $this->types; + } +} diff --git a/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php b/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php new file mode 100644 index 0000000000000..14ae23eb3fe37 --- /dev/null +++ b/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Policy\Renderer; + +use Magento\Csp\Api\Data\ModeConfiguredInterface; +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Api\ModeConfigManagerInterface; +use Magento\Csp\Api\PolicyRendererInterface; +use Magento\Csp\Model\Policy\SimplePolicyInterface; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; + +/** + * Renders a simple policy as a "Content-Security-Policy" header. + */ +class SimplePolicyHeaderRenderer implements PolicyRendererInterface +{ + /** + * @var ModeConfigManagerInterface + */ + private $modeConfig; + + /** + * @param ModeConfigManagerInterface $modeConfig + */ + public function __construct(ModeConfigManagerInterface $modeConfig) + { + $this->modeConfig = $modeConfig; + } + + /** + * @inheritDoc + */ + public function render(PolicyInterface $policy, HttpResponse $response): void + { + /** @var SimplePolicyInterface $policy */ + $config = $this->modeConfig->getConfigured(); + if ($config->isReportOnly()) { + $header = 'Content-Security-Policy-Report-Only'; + } else { + $header = 'Content-Security-Policy'; + } + $value = $policy->getId() .' ' .$policy->getValue() .';'; + if ($config->getReportUri()) { + $reportToData = [ + 'group' => 'report-endpoint', + 'max_age' => 10886400, + 'endpoints' => [ + ['url' => $config->getReportUri()] + ] + ]; + $value .= ' report-uri ' .$config->getReportUri() .';'; + $value .= ' report-to '. $reportToData['group'] .';'; + $response->setHeader('Report-To', json_encode($reportToData), true); + } + $response->setHeader($header, $value, false); + } + + /** + * @inheritDoc + */ + public function canRender(PolicyInterface $policy): bool + { + return true; + } +} diff --git a/app/code/Magento/Csp/Model/Policy/SandboxPolicy.php b/app/code/Magento/Csp/Model/Policy/SandboxPolicy.php new file mode 100644 index 0000000000000..33e3b06f56aec --- /dev/null +++ b/app/code/Magento/Csp/Model/Policy/SandboxPolicy.php @@ -0,0 +1,278 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Policy; + +/** + * "sandbox" directive enables sandbox mode for requested pages limiting their functionality. + * + * Works the same as "sandbox" attribute for iframes but for the main document. + */ +class SandboxPolicy implements SimplePolicyInterface +{ + /** + * @var bool + */ + private $formAllowed; + + /** + * @var bool + */ + private $modalsAllowed; + + /** + * @var bool + */ + private $orientationLockAllowed; + + /** + * @var bool + */ + private $pointerLockAllowed; + + /** + * @var bool + */ + private $popupsAllowed; + + /** + * @var bool + */ + private $popupsToEscapeSandboxAllowed; + + /** + * @var bool + */ + private $presentationAllowed; + + /** + * @var bool + */ + private $sameOriginAllowed; + + /** + * @var bool + */ + private $scriptsAllowed; + + /** + * @var bool + */ + private $topNavigationAllowed; + + /** + * @var bool + */ + private $topNavigationByUserActivationAllowed; + + /** + * @param bool $formAllowed + * @param bool $modalsAllowed + * @param bool $orientationLockAllowed + * @param bool $pointerLockAllowed + * @param bool $popupsAllowed + * @param bool $popupsToEscapeSandboxAllowed + * @param bool $presentationAllowed + * @param bool $sameOriginAllowed + * @param bool $scriptsAllowed + * @param bool $topNavigationAllowed + * @param bool $topNavigationByUserActivationAllowed + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + bool $formAllowed, + bool $modalsAllowed, + bool $orientationLockAllowed, + bool $pointerLockAllowed, + bool $popupsAllowed, + bool $popupsToEscapeSandboxAllowed, + bool $presentationAllowed, + bool $sameOriginAllowed, + bool $scriptsAllowed, + bool $topNavigationAllowed, + bool $topNavigationByUserActivationAllowed + ) { + $this->formAllowed = $formAllowed; + $this->modalsAllowed = $modalsAllowed; + $this->orientationLockAllowed = $orientationLockAllowed; + $this->pointerLockAllowed = $pointerLockAllowed; + $this->popupsAllowed = $popupsAllowed; + $this->popupsToEscapeSandboxAllowed = $popupsToEscapeSandboxAllowed; + $this->presentationAllowed = $presentationAllowed; + $this->sameOriginAllowed = $sameOriginAllowed; + $this->scriptsAllowed = $scriptsAllowed; + $this->topNavigationAllowed = $topNavigationAllowed; + $this->topNavigationByUserActivationAllowed = $topNavigationByUserActivationAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isFormAllowed(): bool + { + return $this->formAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isModalsAllowed(): bool + { + return $this->modalsAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isOrientationLockAllowed(): bool + { + return $this->orientationLockAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isPointerLockAllowed(): bool + { + return $this->pointerLockAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isPopupsAllowed(): bool + { + return $this->popupsAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isPopupsToEscapeSandboxAllowed(): bool + { + return $this->popupsToEscapeSandboxAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isPresentationAllowed(): bool + { + return $this->presentationAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isSameOriginAllowed(): bool + { + return $this->sameOriginAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isScriptsAllowed(): bool + { + return $this->scriptsAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isTopNavigationAllowed(): bool + { + return $this->topNavigationAllowed; + } + + /** + * Sandbox option. + * + * @return bool + */ + public function isTopNavigationByUserActivationAllowed(): bool + { + return $this->topNavigationByUserActivationAllowed; + } + + /** + * @inheritDoc + */ + public function getId(): string + { + return 'sandbox'; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function getValue(): string + { + $allowed = []; + + if ($this->isFormAllowed()) { + $allowed[] = 'allow-forms'; + } + if ($this->isModalsAllowed()) { + $allowed[] = 'allow-modals'; + } + if ($this->isOrientationLockAllowed()) { + $allowed[] = 'allow-orientation-lock'; + } + if ($this->isPointerLockAllowed()) { + $allowed[] = 'allow-pointer-lock'; + } + if ($this->isPopupsAllowed()) { + $allowed[] = 'allow-popups'; + } + if ($this->isPopupsToEscapeSandboxAllowed()) { + $allowed[] = 'allow-popups-to-escape-sandbox'; + } + if ($this->isPresentationAllowed()) { + $allowed[] = 'allow-presentation'; + } + if ($this->isSameOriginAllowed()) { + $allowed[] = 'allow-same-origin'; + } + if ($this->isScriptsAllowed()) { + $allowed[] = 'allow-scripts'; + } + if ($this->isTopNavigationAllowed()) { + $allowed[] = 'allow-top-navigation'; + } + if ($this->isTopNavigationByUserActivationAllowed()) { + $allowed[] = 'allow-top-navigation-by-user-activation'; + } + + if (!$allowed) { + throw new \RuntimeException('At least 1 option must be selected'); + } + return implode(' ', $allowed); + } +} diff --git a/app/code/Magento/Csp/Model/Policy/SimplePolicyInterface.php b/app/code/Magento/Csp/Model/Policy/SimplePolicyInterface.php new file mode 100644 index 0000000000000..aea28a1469de5 --- /dev/null +++ b/app/code/Magento/Csp/Model/Policy/SimplePolicyInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Policy; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Simple policy that is represented by the default prefix and an ID - string value combination. + */ +interface SimplePolicyInterface extends PolicyInterface +{ + +} diff --git a/app/code/Magento/Csp/Model/PolicyRendererPool.php b/app/code/Magento/Csp/Model/PolicyRendererPool.php new file mode 100644 index 0000000000000..5bce191e3a878 --- /dev/null +++ b/app/code/Magento/Csp/Model/PolicyRendererPool.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Api\PolicyRendererInterface; + +/** + * Pool of policy renderers. + */ +class PolicyRendererPool +{ + /** + * @var PolicyRendererInterface[] + */ + private $renderers; + + /** + * @param PolicyRendererInterface[] $renderers + */ + public function __construct(array $renderers) + { + $this->renderers = $renderers; + } + + /** + * Get renderer for the given policy. + * + * @param PolicyInterface $forPolicy + * @return PolicyRendererInterface + * @throws \RuntimeException When it's impossible to find a proper renderer. + */ + public function getRenderer(PolicyInterface $forPolicy): PolicyRendererInterface + { + foreach ($this->renderers as $renderer) { + if ($renderer->canRender($forPolicy)) { + return $renderer; + } + } + + throw new \RuntimeException(sprintf('Failed to find a renderer for policy #%s', $forPolicy->getId())); + } +} diff --git a/app/code/Magento/Csp/Observer/Render.php b/app/code/Magento/Csp/Observer/Render.php new file mode 100644 index 0000000000000..2b88d685f3fe4 --- /dev/null +++ b/app/code/Magento/Csp/Observer/Render.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Observer; + +use Magento\Csp\Api\CspRendererInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; + +/** + * Adds CSP rendering after HTTP response is generated. + */ +class Render implements ObserverInterface +{ + /** + * @var CspRendererInterface + */ + private $cspRenderer; + + /** + * @param CspRendererInterface $cspRenderer + */ + public function __construct(CspRendererInterface $cspRenderer) + { + $this->cspRenderer = $cspRenderer; + } + + /** + * @inheritDoc + */ + public function execute(Observer $observer) + { + /** @var HttpResponse $response */ + $response = $observer->getEvent()->getData('response'); + + $this->cspRenderer->render($response); + } +} diff --git a/app/code/Magento/Csp/README.md b/app/code/Magento/Csp/README.md new file mode 100644 index 0000000000000..47f0f196becd5 --- /dev/null +++ b/app/code/Magento/Csp/README.md @@ -0,0 +1,2 @@ +Magento_Csp implements Content Security Policies for Magento. Allows CSP configuration for Merchants, +provides a way for extension and theme developers to configure CSP headers for their extensions. diff --git a/app/code/Magento/Csp/composer.json b/app/code/Magento/Csp/composer.json new file mode 100644 index 0000000000000..cd8a47a92159d --- /dev/null +++ b/app/code/Magento/Csp/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-csp", + "description": "CSP module enables Content Security Policies for Magento", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-store": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Csp\\": "" + } + } +} diff --git a/app/code/Magento/Csp/etc/adminhtml/events.xml b/app/code/Magento/Csp/etc/adminhtml/events.xml new file mode 100644 index 0000000000000..f81031d2a779a --- /dev/null +++ b/app/code/Magento/Csp/etc/adminhtml/events.xml @@ -0,0 +1,12 @@ +<?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:Event/etc/events.xsd"> + <event name="controller_front_send_response_before"> + <observer name="csp_render" instance="Magento\Csp\Observer\Render" /> + </event> +</config> diff --git a/app/code/Magento/Csp/etc/config.xml b/app/code/Magento/Csp/etc/config.xml new file mode 100644 index 0000000000000..e45f6b223ed22 --- /dev/null +++ b/app/code/Magento/Csp/etc/config.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:module:Magento_Store:etc/config.xsd"> + <default> + <csp> + <mode> + <storefront> + <report_only>1</report_only> + </storefront> + <admin> + <report_only>1</report_only> + </admin> + </mode> + </csp> + </default> +</config> diff --git a/app/code/Magento/Csp/etc/csp_whitelist.xsd b/app/code/Magento/Csp/etc/csp_whitelist.xsd new file mode 100644 index 0000000000000..e68e596e21c79 --- /dev/null +++ b/app/code/Magento/Csp/etc/csp_whitelist.xsd @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Structure description for csp_whitelist.xml configuration files. + * + * 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="csp_whitelist" type="cspWhitelistType" /> + + <xs:complexType name="cspWhitelistType"> + <xs:sequence> + <xs:element name="policies" type="policiesType" minOccurs="1" maxOccurs="1"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="policiesType"> + <xs:sequence> + <xs:element name="policy" type="policyType" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="policyType"> + <xs:sequence> + <xs:element name="values" type="valuesType" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + <xs:attribute name="id" type="xs:string" use="required" /> + </xs:complexType> + + <xs:complexType name="valuesType"> + <xs:sequence> + <xs:element name="value" type="valueType" minOccurs="1" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + <xs:complexType name="valueType"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute type="xs:string" name="id" use="required" /> + <xs:attribute type="cspValueType" name="type" use="required" /> + <xs:attribute type="xs:string" name="algorithm" use="optional" /> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:simpleType name="cspValueType"> + <xs:restriction base="xs:string"> + <xs:enumeration value="host" /> + <xs:enumeration value="hash" /> + </xs:restriction> + </xs:simpleType> +</xs:schema> diff --git a/app/code/Magento/Csp/etc/di.xml b/app/code/Magento/Csp/etc/di.xml new file mode 100644 index 0000000000000..0804f6d579137 --- /dev/null +++ b/app/code/Magento/Csp/etc/di.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:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\Csp\Api\CspRendererInterface" type="Magento\Csp\Model\CspRenderer" /> + <type name="Magento\Csp\Model\PolicyRendererPool"> + <arguments> + <argument name="renderers" xsi:type="array"> + <item name="header" xsi:type="object">Magento\Csp\Model\Policy\Renderer\SimplePolicyHeaderRenderer</item> + </argument> + </arguments> + </type> + <preference for="Magento\Csp\Api\PolicyCollectorInterface" type="Magento\Csp\Model\CompositePolicyCollector" /> + <type name="Magento\Csp\Model\CompositePolicyCollector"> + <arguments> + <argument name="collectors" xsi:type="array"> + <item name="config" xsi:type="object">Magento\Csp\Model\Collector\ConfigCollector</item> + <item name="csp_whitelist" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXmlCollector</item> + </argument> + <argument name="mergers" xsi:type="array"> + <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\FetchPolicyMerger</item> + <item name="flag" xsi:type="object">Magento\Csp\Model\Collector\FlagPolicyMerger</item> + <item name="plugins" xsi:type="object">Magento\Csp\Model\Collector\PluginTypesPolicyMerger</item> + <item name="sandbox" xsi:type="object">Magento\Csp\Model\Collector\SandboxPolicyMerger</item> + </argument> + </arguments> + </type> + <type name="Magento\Csp\Model\Collector\Config\PolicyReaderPool"> + <arguments> + <argument name="readers" xsi:type="array"> + <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\Config\FetchPolicyReader</item> + <item name="plugin_types" xsi:type="object">Magento\Csp\Model\Collector\Config\PluginTypesPolicyReader</item> + <item name="sandbox" xsi:type="object">Magento\Csp\Model\Collector\Config\SandboxPolicyReader</item> + <item name="flag" xsi:type="object">Magento\Csp\Model\Collector\Config\FlagPolicyReader</item> + </argument> + </arguments> + </type> + <preference for="Magento\Csp\Api\ModeConfigManagerInterface" type="Magento\Csp\Model\Mode\ConfigManager" /> + <type name="Magento\Csp\Model\Collector\CspWhitelistXml\Reader"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Converter</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">csp_whitelist.xml</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Csp/etc/frontend/events.xml b/app/code/Magento/Csp/etc/frontend/events.xml new file mode 100644 index 0000000000000..f81031d2a779a --- /dev/null +++ b/app/code/Magento/Csp/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?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:Event/etc/events.xsd"> + <event name="controller_front_send_response_before"> + <observer name="csp_render" instance="Magento\Csp\Observer\Render" /> + </event> +</config> diff --git a/app/code/Magento/Csp/etc/module.xml b/app/code/Magento/Csp/etc/module.xml new file mode 100644 index 0000000000000..e99608ef13f15 --- /dev/null +++ b/app/code/Magento/Csp/etc/module.xml @@ -0,0 +1,10 @@ +<?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_Csp" /> +</config> diff --git a/app/code/Magento/Csp/registration.php b/app/code/Magento/Csp/registration.php new file mode 100644 index 0000000000000..90f4a25452858 --- /dev/null +++ b/app/code/Magento/Csp/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_Csp', __DIR__); diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php index e20054a5a8084..ee94195a29cc7 100644 --- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php +++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php @@ -11,6 +11,9 @@ */ namespace Magento\CurrencySymbol\Block\Adminhtml\System\Currency\Rate; +/** + * Manage currency block + */ class Matrix extends \Magento\Backend\Block\Template { /** @@ -105,7 +108,7 @@ protected function _prepareRates($array) foreach ($array as $key => $rate) { foreach ($rate as $code => $value) { $parts = explode('.', $value); - if (sizeof($parts) == 2) { + if (count($parts) == 2) { $parts[1] = str_pad(rtrim($parts[1], 0), 4, '0', STR_PAD_RIGHT); $array[$key][$code] = join('.', $parts); } elseif ($value > 0) { diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php index 34d24a8b0a7a8..9140807121b83 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php @@ -1,29 +1,75 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Backend\Model\Session as BackendSession; +use Magento\CurrencySymbol\Controller\Adminhtml\System\Currency as CurrencyAction; +use Magento\Directory\Model\Currency\Import\Factory as CurrencyImportFactory; +use Magento\Directory\Model\Currency\Import\ImportInterface as CurrencyImport; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; -use Magento\CurrencySymbol\Controller\Adminhtml\System\Currency as CurrencyAction; +use Magento\Framework\Registry; +use Exception; +/** + * Fetch rates controller. + */ class FetchRates extends CurrencyAction implements HttpGetActionInterface, HttpPostActionInterface { + /** + * @var BackendSession + */ + private $backendSession; + + /** + * @var CurrencyImportFactory + */ + private $currencyImportFactory; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param BackendSession|null $backendSession + * @param CurrencyImportFactory|null $currencyImportFactory + * @param Escaper|null $escaper + */ + public function __construct( + Context $context, + Registry $coreRegistry, + ?BackendSession $backendSession = null, + ?CurrencyImportFactory $currencyImportFactory = null, + ?Escaper $escaper = null + ) { + parent::__construct($context, $coreRegistry); + $this->backendSession = $backendSession ?: ObjectManager::getInstance()->get(BackendSession::class); + $this->currencyImportFactory = $currencyImportFactory ?: ObjectManager::getInstance() + ->get(CurrencyImportFactory::class); + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + } + /** * Fetch rates action * - * @return \Magento\Backend\Model\View\Result\Redirect + * @return Redirect */ public function execute() { - /** @var \Magento\Backend\Model\Session $backendSession */ - $backendSession = $this->_objectManager->get(\Magento\Backend\Model\Session::class); try { $service = $this->getRequest()->getParam('rate_services'); $this->_getSession()->setCurrencyRateService($service); @@ -31,33 +77,33 @@ public function execute() throw new LocalizedException(__('The Import Service is incorrect. Verify the service and try again.')); } try { - /** @var \Magento\Directory\Model\Currency\Import\ImportInterface $importModel */ - $importModel = $this->_objectManager->get(\Magento\Directory\Model\Currency\Import\Factory::class) - ->create($service); - } catch (\Exception $e) { + /** @var CurrencyImport $importModel */ + $importModel = $this->currencyImportFactory->create($service); + } catch (Exception $e) { throw new LocalizedException( __("The import model can't be initialized. Verify the model and try again.") ); } $rates = $importModel->fetchRates(); $errors = $importModel->getMessages(); - if (sizeof($errors) > 0) { + if (count($errors) > 0) { foreach ($errors as $error) { - $this->messageManager->addWarning($error); + $escapedError = $this->escaper->escapeHtml($error); + $this->messageManager->addWarningMessage($escapedError); } - $this->messageManager->addWarning( + $this->messageManager->addWarningMessage( __('Click "Save" to apply the rates we found.') ); } else { - $this->messageManager->addSuccess(__('Click "Save" to apply the rates we found.')); + $this->messageManager->addSuccessMessage(__('Click "Save" to apply the rates we found.')); } - $backendSession->setRates($rates); - } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->backendSession->setRates($rates); + } catch (Exception $e) { + $this->messageManager->addErrorMessage($e->getMessage()); } - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setPath('adminhtml/*/'); } diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php index 8dd6b5e6fac41..f5e1fdbdb0c56 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,6 +8,9 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +/** + * Class SaveRates + */ class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency implements HttpPostActionInterface { /** @@ -23,12 +25,13 @@ public function execute() try { foreach ($data as $currencyCode => $rate) { foreach ($rate as $currencyTo => $value) { - $value = abs($this->_objectManager->get( - \Magento\Framework\Locale\FormatInterface::class - )->getNumber($value)); + $value = abs( + $this->_objectManager->get(\Magento\Framework\Locale\FormatInterface::class) + ->getNumber($value) + ); $data[$currencyCode][$currencyTo] = $value; if ($value == 0) { - $this->messageManager->addWarning( + $this->messageManager->addWarningMessage( __('Please correct the input data for "%1 => %2" rate.', $currencyCode, $currencyTo) ); } @@ -36,9 +39,9 @@ public function execute() } $this->_objectManager->create(\Magento\Directory\Model\Currency::class)->saveRates($data); - $this->messageManager->addSuccess(__('All valid rates have been saved.')); + $this->messageManager->addSuccessMessage(__('All valid rates have been saved.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php index 703117f34fce6..f77976cc9e2f2 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,9 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +/** + * Class Save + */ class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol implements HttpPostActionInterface { /** @@ -29,9 +31,9 @@ public function execute() try { $this->_objectManager->create(\Magento\CurrencySymbol\Model\System\Currencysymbol::class) ->setCurrencySymbolsData($symbolsDataArray); - $this->messageManager->addSuccess(__('You applied the custom currency symbols.')); + $this->messageManager->addSuccessMessage(__('You applied the custom currency symbols.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminCurrencyRatesActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminCurrencyRatesActionGroup.xml new file mode 100644 index 0000000000000..a2e25cf858a6e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminCurrencyRatesActionGroup.xml @@ -0,0 +1,35 @@ +<?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="AdminImportCurrencyRatesActionGroup"> + <arguments> + <argument name="rateService" type="string" defaultValue="Fixer.io"/> + </arguments> + <selectOption selector="{{AdminCurrencyRatesSection.rateService}}" userInput="{{rateService}}" stepKey="selectRateService"/> + <click selector="{{AdminCurrencyRatesSection.import}}" stepKey="clickImport"/> + <waitForElementVisible selector="{{AdminCurrencyRatesSection.oldRate}}" stepKey="waitForOldRateVisible"/> + </actionGroup> + <actionGroup name="AdminSaveCurrencyRatesActionGroup"> + <click selector="{{AdminCurrencyRatesSection.saveCurrencyRates}}" stepKey="clickSaveCurrencyRates"/> + <waitForPageLoad stepKey="waitForSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="All valid rates have been saved." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AdminSetCurrencyRatesActionGroup"> + <arguments> + <argument name="firstCurrency" type="string" defaultValue="USD"/> + <argument name="secondCurrency" type="string" defaultValue="EUR"/> + <argument name="rate" type="string" defaultValue="0.5"/> + </arguments> + <fillField selector="{{AdminCurrencyRatesSection.currencyRate(firstCurrency, secondCurrency)}}" userInput="{{rate}}" stepKey="setCurrencyRate"/> + <click selector="{{AdminCurrencyRatesSection.saveCurrencyRates}}" stepKey="clickSaveCurrencyRates"/> + <waitForPageLoad stepKey="waitForSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="{{AdminSaveCurrencyRatesMessageData.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontCurrencyRatesActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontCurrencyRatesActionGroup.xml new file mode 100644 index 0000000000000..02cf23c323a8a --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontCurrencyRatesActionGroup.xml @@ -0,0 +1,29 @@ +<?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="StorefrontSwitchCurrencyActionGroup"> + <arguments> + <argument name="currency" type="string" defaultValue="EUR"/> + </arguments> + <click selector="{{StorefrontSwitchCurrencyRatesSection.currencyToggle}}" stepKey="openToggle"/> + <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="waitForCurrency"/> + <click selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="chooseCurrency"/> + <see selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" userInput="{{currency}}" stepKey="seeSelectedCurrency"/> + </actionGroup> + <actionGroup name="StorefrontSwitchCurrency"> + <arguments> + <argument name="currency" type="string" defaultValue="EUR"/> + </arguments> + <click selector="{{StorefrontSwitchCurrencyRatesSection.currencyTrigger}}" stepKey="openTrigger"/> + <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="waitForCurrency"/> + <click selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="chooseCurrency"/> + <see selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" userInput="{{currency}}" stepKey="seeSelectedCurrency"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminCurrencyRatesMessageData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminCurrencyRatesMessageData.xml new file mode 100644 index 0000000000000..90d22b06fcb80 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminCurrencyRatesMessageData.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="AdminSaveCurrencyRatesMessageData"> + <data key="success">All valid rates have been saved.</data> + </entity> +</entities> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml new file mode 100644 index 0000000000000..6194287dd058b --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml @@ -0,0 +1,53 @@ +<?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="SetCurrencyUSDBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">USD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetCurrencyEURBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">EUR</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForUSD"> + <data key="path">currency/options/allow</data> + <data key="value">USD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForEUR"> + <data key="path">currency/options/allow</data> + <data key="value">EUR</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForRUB"> + <data key="path">currency/options/allow</data> + <data key="value">RUB</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyEURConfig"> + <data key="path">currency/options/default</data> + <data key="value">EUR</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyUSDConfig"> + <data key="path">currency/options/default</data> + <data key="value">USD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> +</entities> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Page/AdminCurrencyRatesPage.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/AdminCurrencyRatesPage.xml new file mode 100644 index 0000000000000..d31dd71d474bb --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/AdminCurrencyRatesPage.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="AdminCurrencyRatesPage" url="admin/system_currency/" area="admin" module="CurrencySymbol"> + <section name="AdminCurrencyRatesSection"/> + </page> +</pages> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml new file mode 100644 index 0000000000000..bc80a51c41c47 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.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="AdminCurrencyRatesSection"> + <element name="import" type="button" selector="//button[@title='Import']"/> + <element name="saveCurrencyRates" type="button" selector="//button[@title='Save Currency Rates']"/> + <element name="oldRate" type="text" selector="//div[contains(@class, 'admin__field-note') and contains(text(), 'Old rate:')]/strong"/> + <element name="rateService" type="select" selector="#rate_services"/> + <element name="currencyRate" type="input" selector="input[name='rate[{{fistCurrency}}][{{secondCurrency}}]']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml new file mode 100644 index 0000000000000..ff922421e5db7 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.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="StorefrontSwitchCurrencyRatesSection"> + <element name="currencyToggle" type="select" selector="#switcher-currency-trigger" timeout="30"/> + <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> + <element name="currency" type="button" selector="//div[@id='switcher-currency-trigger']/following-sibling::ul//a[contains(text(), '{{currency}}')]" parameterized="true" timeout="10"/> + <element name="selectedCurrency" type="text" selector="#switcher-currency-trigger span"/> + </section> +</sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml new file mode 100644 index 0000000000000..6232f59bb839a --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml @@ -0,0 +1,79 @@ +<?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="AdminCurrencyConverterAPIConfigurationTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Currency Rates"/> + <title value="Currency Converter API configuration"/> + <description value="Currency Converter API configuration"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-19272"/> + <useCaseId value="MAGETWO-94919"/> + <group value="currency"/> + <skip> + <issueId value="MQE-1578"/> + </skip> + </annotations> + <before> + <!--Set currency allow config--> + <magentoCLI command="config:set currency/options/allow RHD,CHW,CHE,AMD,EUR,USD" stepKey="setCurrencyAllow"/> + <!--TODO: Add Api key--> + <magentoCLI command="cache:flush" stepKey="clearCache"/> + <!--Create product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Set currency allow previous config--> + <magentoCLI command="config:set currency/options/allow EUR,USD" stepKey="setCurrencyAllow"/> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Import rates from Currency Converter API--> + <amOnPage url="{{AdminCurrencyRatesPage.url}}" stepKey="onCurrencyRatePage"/> + <actionGroup ref="AdminImportCurrencyRatesActionGroup" stepKey="importCurrencyRates"> + <argument name="rateService" value="Currency Converter API"/> + </actionGroup> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput='Click "Save" to apply the rates we found.' stepKey="seeImportMessage"/> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput="We can't retrieve a rate from https://free.currconv.com for CHE." stepKey="seeWarningMessageForCHE"/> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput="We can't retrieve a rate from https://free.currconv.com for RHD." stepKey="seeWarningMessageForRHD"/> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput="We can't retrieve a rate from https://free.currconv.com for CHW." stepKey="seeWarningMessageForCHW"/> + <actionGroup ref="AdminSaveCurrencyRatesActionGroup" stepKey="saveCurrencyRates"/> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput='Please correct the input data for "USD => CHE" rate' stepKey="seeCHEMessageAfterSave"/> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput='Please correct the input data for "USD => RHD" rate' stepKey="seeRHDMessageAfterSave"/> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput='Please correct the input data for "USD => CHW" rate' stepKey="seeCHWMessageAfterSave"/> + <!--Go to the Storefront and check currency rates--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontSwitchCurrencyActionGroup" stepKey="switchAMDCurrency"> + <argument name="currency" value="AMD"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="AMD" stepKey="seeAMDInPrice"/> + <actionGroup ref="StorefrontSwitchCurrencyActionGroup" stepKey="switchEURCurrency"> + <argument name="currency" value="EUR"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="€" stepKey="seeEURInPrice"/> + <!--Set allowed currencies greater then 10--> + <magentoCLI command="config:set currency/options/allow RHD,CHW,YER,ZMK,CHE,EUR,USD,AMD,RUB,DZD,ARS,AWG" stepKey="setCurrencyAllow"/> + <magentoCLI command="cache:flush" stepKey="clearCache"/> + <!--Import rates from Currency Converter API with currencies greater then 10--> + <amOnPage url="{{AdminCurrencyRatesPage.url}}" stepKey="onCurrencyRatePageSecondTime"/> + <actionGroup ref="AdminImportCurrencyRatesActionGroup" stepKey="importCurrencyRatesGreaterThen10"> + <argument name="rateService" value="Currency Converter API"/> + </actionGroup> + <see selector="{{AdminMessagesSection.warningMessage}}" userInput="Too many pairs. Maximum of 10 is supported for the free version." stepKey="seeTooManyPairsMessage"/> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest.xml new file mode 100644 index 0000000000000..26fbfd394be68 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest.xml @@ -0,0 +1,75 @@ +<?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="AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Currency rates order page"/> + <title value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct"/> + <description value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17255" /> + <useCaseId value="MAGETWO-67450"/> + <group value="currency"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createNewProduct"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseUSDWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}},{{SetAllowedCurrenciesConfigForRUB.value}}" stepKey="setAllowedCurrencyWebsitesEURandRUBandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyEURConfig.scope}} --scope-code={{SetDefaultCurrencyEURConfig.scope_code}} {{SetDefaultCurrencyEURConfig.path}} {{SetDefaultCurrencyEURConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + </before> + <after> + <!--Delete created product--> + <comment userInput="Delete created product" stepKey="commentDeleteCreatedProduct"/> + <deleteData createDataKey="createNewProduct" stepKey="deleteNewProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Set currency rates--> + <amOnPage url="{{AdminCurrencyRatesPage.url}}" stepKey="gotToCurrencyRatesPageSecondTime"/> + <waitForPageLoad stepKey="waitForLoadRatesPageSecondTime"/> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="setCurrencyRates"> + <argument name="firstCurrency" value="USD"/> + <argument name="secondCurrency" value="RUB"/> + <argument name="rate" value="0.8"/> + </actionGroup> + <!--Open created product on Storefront and place for order--> + <amOnPage url="{{StorefrontProductPage.url($$createNewProduct.custom_attributes[url_key]$$)}}" stepKey="goToNewProductPage"/> + <waitForPageLoad stepKey="waitForNewProductPagePageLoad"/> + <actionGroup ref="StorefrontSwitchCurrency" stepKey="switchCurrency"> + <argument name="currency" value="RUB"/> + </actionGroup> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontNewProductPage"> + <argument name="productName" value="$$createNewProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutNewProductFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutNewFillingShippingSection"> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectNewCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceNewOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabNewOrderNumber"/> + <!--Open order and check rates display in one line--> + <actionGroup ref="OpenOrderById" stepKey="openNewOrderById"> + <argument name="orderId" value="$grabNewOrderNumber"/> + </actionGroup> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="EUR / USD rate" stepKey="seeUSDandEURRate"/> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="RUB / USD rate:" stepKey="seeRUBandEURRate"/> + <grabMultiple selector="{{AdminOrderDetailsInformationSection.rate}}" stepKey="grabRates" /> + <assertEquals stepKey="assertRates"> + <actualResult type="variable">grabRates</actualResult> + <expectedResult type="array">['EUR / USD rate:', 'RUB / USD rate:']</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayedInOneLineTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayedInOneLineTest.xml new file mode 100644 index 0000000000000..dc6bdf3db542e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayedInOneLineTest.xml @@ -0,0 +1,77 @@ +<?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="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Currency rates order page"/> + <title value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct once"/> + <description value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct once"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17255" /> + <useCaseId value="MAGETWO-67450"/> + <group value="currency"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!--Set price scope website--> + <magentoCLI command="config:set {{CatalogPriceScopeWebsiteConfigData.path}} {{CatalogPriceScopeWebsiteConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyEURBaseConfig.path}} {{SetCurrencyEURBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyEURConfig.path}} {{SetDefaultCurrencyEURConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyEURConfig.scope}} --scope-code={{SetDefaultCurrencyEURConfig.scope_code}} {{SetDefaultCurrencyEURConfig.path}} {{SetDefaultCurrencyEURConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + </before> + <after> + <!--Delete created product--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!--Reset configurations--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeGlobal"/> + <magentoCLI command="config:set {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyUSDConfig.path}} {{SetDefaultCurrencyUSDConfig.value}}" stepKey="setCurrencyDefaultUSD"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}}" stepKey="setAllowedCurrencyUSD"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseUSDWebsites"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyUSDConfig.scope}} --scope-code={{SetDefaultCurrencyUSDConfig.scope_code}} {{SetDefaultCurrencyUSDConfig.path}} {{SetDefaultCurrencyUSDConfig.value}}" stepKey="setCurrencyDefaultUSDWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}}" stepKey="setAllowedCurrencyUSDWebsites"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open created product on Storefront and place for order--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPagePageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!--Open order and check rates display in one line--> + <actionGroup ref="OpenOrderById" stepKey="openOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="EUR / USD rate" stepKey="seeEURandUSDRate"/> + <grabMultiple selector="{{AdminOrderDetailsInformationSection.rate}}" stepKey="grabRate" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[EUR / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currency/SaveRatesTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currency/SaveRatesTest.php new file mode 100644 index 0000000000000..b561c02c7b36e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currency/SaveRatesTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CurrencySymbol\Test\Unit\Controller\Adminhtml\System\Currency; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class SaveRatesTest + */ +class SaveRatesTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates + */ + protected $action; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $requestMock; + + /** + * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $responseMock; + + /** + * + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + + $this->responseMock = $this->createPartialMock( + \Magento\Framework\App\ResponseInterface::class, + ['setRedirect', 'sendResponse'] + ); + + $this->action = $objectManager->getObject( + \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates::class, + [ + 'request' => $this->requestMock, + 'response' => $this->responseMock, + ] + ); + } + + /** + * + */ + public function testWithNullRateExecute() + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('rate') + ->willReturn(null); + + $this->responseMock->expects($this->once())->method('setRedirect'); + + $this->action->execute(); + } +} diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php index 0863104a2bf8d..06f4294ce6397 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php @@ -128,7 +128,7 @@ public function testExecute() ->willReturn($this->filterManagerMock); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You applied the custom currency symbols.')); $this->action->execute(); diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php index db560f7de3ecb..3709f4914c477 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php @@ -75,7 +75,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _construct() { @@ -119,7 +119,7 @@ protected function _prepareCollection() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -201,7 +201,7 @@ public function getCustomerId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getGridUrl() { @@ -224,7 +224,13 @@ public function getGridParentHtml() */ public function getRowUrl($row) { - return $this->getUrl('catalog/product/edit', ['id' => $row->getProductId()]); + return $this->getUrl( + 'catalog/product/edit', + [ + 'id' => $row->getProductId(), + 'customerId' => $this->getCustomerId() + ] + ); } /** diff --git a/app/code/Magento/Customer/Block/Form/Register.php b/app/code/Magento/Customer/Block/Form/Register.php index be16046d69075..46d1088e37d0f 100644 --- a/app/code/Magento/Customer/Block/Form/Register.php +++ b/app/code/Magento/Customer/Block/Form/Register.php @@ -24,7 +24,7 @@ class Register extends \Magento\Directory\Block\Data protected $_customerSession; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; @@ -47,7 +47,7 @@ class Register extends \Magento\Directory\Block\Data * @param \Magento\Framework\App\Cache\Type\Config $configCacheType * @param \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionCollectionFactory * @param \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countryCollectionFactory - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Url $customerUrl * @param array $data @@ -62,7 +62,7 @@ public function __construct( \Magento\Framework\App\Cache\Type\Config $configCacheType, \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionCollectionFactory, \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $countryCollectionFactory, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Url $customerUrl, array $data = [], diff --git a/app/code/Magento/Customer/Block/SectionNamesProvider.php b/app/code/Magento/Customer/Block/SectionNamesProvider.php new file mode 100644 index 0000000000000..92029d1715d4b --- /dev/null +++ b/app/code/Magento/Customer/Block/SectionNamesProvider.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Block; + +use Magento\Customer\CustomerData\SectionPool; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * ViewModel to get sections names array. + */ +class SectionNamesProvider implements ArgumentInterface +{ + /** + * @var SectionPool + */ + private $sectionPool; + + /** + * @param SectionPool $sectionPool + */ + public function __construct( + SectionPool $sectionPool + ) { + $this->sectionPool = $sectionPool; + } + + /** + * Return array of section names based on config. + * + * @return array + */ + public function getSectionNames() + { + return $this->sectionPool->getSectionNames(); + } +} diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index d874729d9132e..e020de79a3a60 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -267,6 +267,8 @@ public function getHtmlExtraParams() $validators['validate-date'] = [ 'dateFormat' => $this->getDateFormat() ]; + $validators['validate-dob'] = true; + return 'data-validate="' . $this->_escaper->escapeHtml(json_encode($validators)) . '"'; } @@ -277,7 +279,11 @@ public function getHtmlExtraParams() */ public function getDateFormat() { - return $this->_localeDate->getDateFormatWithLongYear(); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); + /** Escape RTL characters which are present in some locales and corrupt formatting */ + $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + + return $escapedDateFormat; } /** diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index adca90c5e5f24..3aa08cfbf847e 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -168,7 +168,7 @@ public function execute() $metadata->setPath('/'); $this->getCookieManager()->deleteCookie('mage-cache-sessid', $metadata); } - $this->messageManager->addSuccess($this->getSuccessMessage()); + $this->messageManager->addSuccessMessage($this->getSuccessMessage()); $resultRedirect->setUrl($this->getSuccessRedirect()); return $resultRedirect; } catch (StateException $e) { diff --git a/app/code/Magento/Customer/Controller/Account/Confirmation.php b/app/code/Magento/Customer/Controller/Account/Confirmation.php index a3e2db0207630..59def8640328c 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirmation.php +++ b/app/code/Magento/Customer/Controller/Account/Confirmation.php @@ -1,21 +1,26 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Controller\AbstractAccount; +use Magento\Customer\Model\Session; use Magento\Customer\Model\Url; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Action\Context; -use Magento\Customer\Model\Session; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\State\InvalidTransitionException; use Magento\Framework\View\Result\PageFactory; use Magento\Store\Model\StoreManagerInterface; -use Magento\Customer\Api\AccountManagementInterface; -use Magento\Framework\Exception\State\InvalidTransitionException; -class Confirmation extends \Magento\Customer\Controller\AbstractAccount +/** + * Class Confirmation. Send confirmation link to specified email + */ +class Confirmation extends AbstractAccount implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Store\Model\StoreManagerInterface @@ -91,11 +96,11 @@ public function execute() $email, $this->storeManager->getStore()->getWebsiteId() ); - $this->messageManager->addSuccess(__('Please check your email for confirmation key.')); + $this->messageManager->addSuccessMessage(__('Please check your email for confirmation key.')); } catch (InvalidTransitionException $e) { - $this->messageManager->addSuccess(__('This email does not require confirmation.')); + $this->messageManager->addSuccessMessage(__('This email does not require confirmation.')); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Wrong email.')); + $this->messageManager->addExceptionMessage($e, __('Wrong email.')); $resultRedirect->setPath('*/*/*', ['email' => $email, '_secure' => true]); return $resultRedirect; } diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index a2be0f68b56cb..a006cfe6725f3 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -21,6 +21,7 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\MessageInterface; use Magento\Framework\Phrase; use Magento\Store\Model\StoreManagerInterface; use Magento\Customer\Api\AccountManagementInterface; @@ -118,6 +119,11 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface, Ht */ protected $session; + /** + * @var StoreManagerInterface + */ + protected $storeManager; + /** * @var AccountRedirect */ @@ -365,20 +371,17 @@ public function execute() ); $confirmationStatus = $this->accountManagement->getConfirmationStatus($customer->getId()); if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { - $email = $this->customerUrl->getEmailConfirmationUrl($customer->getEmail()); - // @codingStandardsIgnoreStart - $this->messageManager->addSuccess( - __( - 'You must confirm your account. Please check your email for the confirmation link or <a href="%1">click here</a> for a new link.', - $email - ) + $this->messageManager->addComplexSuccessMessage( + 'confirmAccountSuccessMessage', + [ + 'url' => $this->customerUrl->getEmailConfirmationUrl($customer->getEmail()), + ] ); - // @codingStandardsIgnoreEnd $url = $this->urlModel->getUrl('*/*/index', ['_secure' => true]); $resultRedirect->setUrl($this->_redirect->success($url)); } else { $this->session->setCustomerDataAsLoggedIn($customer); - $this->messageManager->addSuccess($this->getSuccessMessage()); + $this->messageManager->addMessage($this->getMessageManagerSuccessMessage()); $requestedRedirect = $this->accountRedirect->getRedirectCookie(); if (!$this->scopeConfig->getValue('customer/startup/redirect_dashboard') && $requestedRedirect) { $resultRedirect->setUrl($this->_redirect->success($requestedRedirect)); @@ -395,23 +398,21 @@ public function execute() return $resultRedirect; } catch (StateException $e) { - $url = $this->urlModel->getUrl('customer/account/forgotpassword'); - // @codingStandardsIgnoreStart - $message = __( - 'There is already an account with this email address. If you are sure that it is your email address, <a href="%1">click here</a> to get your password and access your account.', - $url + $this->messageManager->addComplexErrorMessage( + 'customerAlreadyExistsErrorMessage', + [ + 'url' => $this->urlModel->getUrl('customer/account/forgotpassword'), + ] ); - // @codingStandardsIgnoreEnd - $this->messageManager->addError($message); } catch (InputException $e) { - $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); + $this->messageManager->addErrorMessage($e->getMessage()); foreach ($e->getErrors() as $error) { - $this->messageManager->addError($this->escaper->escapeHtml($error->getMessage())); + $this->messageManager->addErrorMessage($error->getMessage()); } } catch (LocalizedException $e) { - $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t save the customer.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t save the customer.')); } $this->session->setCustomerFormData($this->getRequest()->getPostValue()); @@ -437,6 +438,8 @@ protected function checkPasswordConfirmation($password, $confirmation) /** * Retrieve success message * + * @deprecated + * @see getMessageManagerSuccessMessage() * @return string */ protected function getSuccessMessage() @@ -462,4 +465,37 @@ protected function getSuccessMessage() } return $message; } + + /** + * Retrieve success message manager message + * + * @return MessageInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getMessageManagerSuccessMessage(): MessageInterface + { + if ($this->addressHelper->isVatValidationEnabled()) { + if ($this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING) { + $identifier = 'customerVatShippingAddressSuccessMessage'; + } else { + $identifier = 'customerVatBillingAddressSuccessMessage'; + } + + $message = $this->messageManager + ->createMessage(MessageInterface::TYPE_SUCCESS, $identifier) + ->setData( + [ + 'url' => $this->urlModel->getUrl('customer/address/edit'), + ] + ); + } else { + $message = $this->messageManager + ->createMessage(MessageInterface::TYPE_SUCCESS) + ->setText( + __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ); + } + + return $message; + } } diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index 4eb41cedea29a..04b5b72ae776b 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -216,7 +216,7 @@ public function execute() $isPasswordChanged ); $this->dispatchSuccessEvent($customerCandidateDataObject); - $this->messageManager->addSuccess(__('You saved the account information.')); + $this->messageManager->addSuccessMessage(__('You saved the account information.')); return $resultRedirect->setPath('customer/account'); } catch (InvalidEmailOrPasswordException $e) { $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); @@ -227,7 +227,7 @@ public function execute() ); $this->session->logout(); $this->session->start(); - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); return $resultRedirect->setPath('customer/account/login'); } catch (InputException $e) { $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); @@ -235,7 +235,7 @@ public function execute() $this->messageManager->addErrorMessage($this->escaper->escapeHtml($error->getMessage())); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addException($e, __('We can\'t save the customer.')); } diff --git a/app/code/Magento/Customer/Controller/Account/LoginPost.php b/app/code/Magento/Customer/Controller/Account/LoginPost.php index 04051fbbf366b..36d04e949923f 100644 --- a/app/code/Magento/Customer/Controller/Account/LoginPost.php +++ b/app/code/Magento/Customer/Controller/Account/LoginPost.php @@ -26,6 +26,8 @@ use Magento\Framework\Phrase; /** + * Post login customer action. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class LoginPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface @@ -183,7 +185,6 @@ public function execute() try { $customer = $this->customerAccountManagement->authenticate($login['username'], $login['password']); $this->session->setCustomerDataAsLoggedIn($customer); - $this->session->regenerateId(); if ($this->getCookieManager()->getCookie('mage-cache-sessid')) { $metadata = $this->getCookieMetadataFactory()->createCookieMetadata(); $metadata->setPath('/'); @@ -203,11 +204,6 @@ public function execute() 'This account is not confirmed. <a href="%1">Click here</a> to resend confirmation email.', $value ); - } catch (UserLockedException $e) { - $message = __( - 'The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.' - ); } catch (AuthenticationException $e) { $message = __( 'The account sign-in was incorrect or your account is disabled temporarily. ' @@ -217,17 +213,17 @@ public function execute() $message = $e->getMessage(); } catch (\Exception $e) { // PA DSS violation: throwing or logging an exception here can disclose customer password - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('An unspecified error occurred. Please contact us for assistance.') ); } finally { if (isset($message)) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); $this->session->setUsername($login['username']); } } } else { - $this->messageManager->addError(__('A login and a password are required.')); + $this->messageManager->addErrorMessage(__('A login and a password are required.')); } } diff --git a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php index 27a00f86dd95d..a127f2acf538f 100644 --- a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php @@ -73,13 +73,13 @@ public function execute() $passwordConfirmation = (string)$this->getRequest()->getPost('password_confirmation'); if ($password !== $passwordConfirmation) { - $this->messageManager->addError(__("New Password and Confirm New Password values didn't match.")); + $this->messageManager->addErrorMessage(__("New Password and Confirm New Password values didn't match.")); $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); return $resultRedirect; } if (iconv_strlen($password) <= 0) { - $this->messageManager->addError(__('Please enter a new password.')); + $this->messageManager->addErrorMessage(__('Please enter a new password.')); $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); return $resultRedirect; @@ -92,17 +92,17 @@ public function execute() $password ); $this->session->unsRpToken(); - $this->messageManager->addSuccess(__('You updated your password.')); + $this->messageManager->addSuccessMessage(__('You updated your password.')); $resultRedirect->setPath('*/*/login'); return $resultRedirect; } catch (InputException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); foreach ($e->getErrors() as $error) { - $this->messageManager->addError($error->getMessage()); + $this->messageManager->addErrorMessage($error->getMessage()); } } catch (\Exception $exception) { - $this->messageManager->addError(__('Something went wrong while saving the new password.')); + $this->messageManager->addErrorMessage(__('Something went wrong while saving the new password.')); } $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); diff --git a/app/code/Magento/Customer/Controller/Address/Delete.php b/app/code/Magento/Customer/Controller/Address/Delete.php index a30e15db4b3f8..2024b2c58b8ef 100644 --- a/app/code/Magento/Customer/Controller/Address/Delete.php +++ b/app/code/Magento/Customer/Controller/Address/Delete.php @@ -27,9 +27,9 @@ public function execute() $address = $this->_addressRepository->getById($addressId); if ($address->getCustomerId() === $this->_getSession()->getCustomerId()) { $this->_addressRepository->deleteById($addressId); - $this->messageManager->addSuccess(__('You deleted the address.')); + $this->messageManager->addSuccessMessage(__('You deleted the address.')); } else { - $this->messageManager->addError(__('We can\'t delete the address right now.')); + $this->messageManager->addErrorMessage(__('We can\'t delete the address right now.')); } } catch (\Exception $other) { $this->messageManager->addException($other, __('We can\'t delete the address right now.')); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php index b69410ecbfce7..7747d80595cdc 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php @@ -139,14 +139,14 @@ public function execute() if ($customerId = $this->getRequest()->getParam('customer_id')) { try { $this->tokenService->revokeCustomerAccessToken($customerId); - $this->messageManager->addSuccess(__('You have revoked the customer\'s tokens.')); + $this->messageManager->addSuccessMessage(__('You have revoked the customer\'s tokens.')); $resultRedirect->setPath('customer/index/edit', ['id' => $customerId, '_current' => true]); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('customer/index/edit', ['id' => $customerId, '_current' => true]); } } else { - $this->messageManager->addError(__('We can\'t find a customer to revoke.')); + $this->messageManager->addErrorMessage(__('We can\'t find a customer to revoke.')); $resultRedirect->setPath('customer/index/index'); } return $resultRedirect; diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php index ab32ea08a44aa..661ef1cace69b 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,6 +8,9 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Class Delete + */ class Delete extends \Magento\Customer\Controller\Adminhtml\Group implements HttpPostActionInterface { /** @@ -24,12 +26,12 @@ public function execute() if ($id) { try { $this->groupRepository->deleteById($id); - $this->messageManager->addSuccess(__('You deleted the customer group.')); + $this->messageManager->addSuccessMessage(__('You deleted the customer group.')); } catch (NoSuchEntityException $e) { - $this->messageManager->addError(__('The customer group no longer exists.')); + $this->messageManager->addErrorMessage(__('The customer group no longer exists.')); return $resultRedirect->setPath('customer/*/'); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); return $resultRedirect->setPath('customer/group/edit', ['id' => $id]); } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 5ffce4cbcd989..64c94fa230fb1 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -93,10 +93,10 @@ public function execute() $this->groupRepository->save($customerGroup); - $this->messageManager->addSuccess(__('You saved the customer group.')); + $this->messageManager->addSuccessMessage(__('You saved the customer group.')); $resultRedirect->setPath('customer/group'); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); if ($customerGroup != null) { $this->storeCustomerGroupDataToSession( $this->dataObjectProcessor->buildOutputDataArray( diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php index e26b49aaebe7a..e2bde42351d45 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php @@ -64,7 +64,7 @@ public function execute() $collection = $this->filter->getCollection($this->collectionFactory->create()); return $this->massAction($collection); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setPath($this->redirectUrl); @@ -73,6 +73,7 @@ public function execute() /** * Return component referer url + * * TODO: Technical dept referer url should be implement as a part of Action configuration in appropriate way * * @return null|string diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php index ab39ca098162f..4b2f2614948cf 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php @@ -31,7 +31,7 @@ public function execute() $formKeyIsValid = $this->_formKeyValidator->validate($this->getRequest()); $isPost = $this->getRequest()->isPost(); if (!$formKeyIsValid || !$isPost) { - $this->messageManager->addError(__('Customer could not be deleted.')); + $this->messageManager->addErrorMessage(__('Customer could not be deleted.')); return $resultRedirect->setPath('customer/index'); } @@ -39,9 +39,9 @@ public function execute() if (!empty($customerId)) { try { $this->_customerRepository->deleteById($customerId); - $this->messageManager->addSuccess(__('You deleted the customer.')); + $this->messageManager->addSuccessMessage(__('You deleted the customer.')); } catch (\Exception $exception) { - $this->messageManager->addError($exception->getMessage()); + $this->messageManager->addErrorMessage($exception->getMessage()); } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php index 7220de0356817..eff812a65a3bb 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php @@ -70,6 +70,11 @@ class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionIn */ private $addressRegistry; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @param Action\Context $context * @param CustomerRepositoryInterface $customerRepository @@ -78,6 +83,7 @@ class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionIn * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param \Psr\Log\LoggerInterface $logger * @param AddressRegistry|null $addressRegistry + * @param \Magento\Framework\Escaper $escaper */ public function __construct( Action\Context $context, @@ -86,7 +92,8 @@ public function __construct( \Magento\Customer\Model\Customer\Mapper $customerMapper, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, \Psr\Log\LoggerInterface $logger, - AddressRegistry $addressRegistry = null + AddressRegistry $addressRegistry = null, + \Magento\Framework\Escaper $escaper = null ) { $this->customerRepository = $customerRepository; $this->resultJsonFactory = $resultJsonFactory; @@ -94,6 +101,7 @@ public function __construct( $this->dataObjectHelper = $dataObjectHelper; $this->logger = $logger; $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(\Magento\Framework\Escaper::class); parent::__construct($context); } @@ -128,10 +136,14 @@ public function execute() $postItems = $this->getRequest()->getParam('items', []); if (!($this->getRequest()->getParam('isAjax') && count($postItems))) { - return $resultJson->setData([ - 'messages' => [__('Please correct the data sent.')], - 'error' => true, - ]); + return $resultJson->setData( + [ + 'messages' => [ + __('Please correct the data sent.') + ], + 'error' => true, + ] + ); } foreach (array_keys($postItems) as $customerId) { @@ -147,10 +159,12 @@ public function execute() $this->getEmailNotification()->credentialsChanged($this->getCustomer(), $currentCustomer->getEmail()); } - return $resultJson->setData([ - 'messages' => $this->getErrorMessages(), - 'error' => $this->isErrorExists() - ]); + return $resultJson->setData( + [ + 'messages' => $this->getErrorMessages(), + 'error' => $this->isErrorExists() + ] + ); } /** @@ -234,13 +248,16 @@ protected function saveCustomer(CustomerInterface $customer) $this->disableAddressValidation($customer); $this->customerRepository->save($customer); } catch (\Magento\Framework\Exception\InputException $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); + $this->getMessageManager() + ->addError($this->getErrorWithCustomerId($this->escaper->escapeHtml($e->getMessage()))); $this->logger->critical($e); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); + $this->getMessageManager() + ->addError($this->getErrorWithCustomerId($this->escaper->escapeHtml($e->getMessage()))); $this->logger->critical($e); } catch (\Exception $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId('We can\'t save the customer.')); + $this->getMessageManager() + ->addError($this->getErrorWithCustomerId('We can\'t save the customer.')); $this->logger->critical($e); } } diff --git a/app/code/Magento/Customer/Controller/Ajax/Login.php b/app/code/Magento/Customer/Controller/Ajax/Login.php index 5049c83e60f35..1215c47ab9902 100644 --- a/app/code/Magento/Customer/Controller/Ajax/Login.php +++ b/app/code/Magento/Customer/Controller/Ajax/Login.php @@ -191,7 +191,6 @@ public function execute() $credentials['password'] ); $this->customerSession->setCustomerDataAsLoggedIn($customer); - $this->customerSession->regenerateId(); $redirectRoute = $this->getAccountRedirect()->getRedirectCookie(); if ($this->cookieManager->getCookie('mage-cache-sessid')) { $metadata = $this->cookieMetadataFactory->createCookieMetadata(); diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php index efea1762d9de6..eef2854cf363e 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPool.php +++ b/app/code/Magento/Customer/CustomerData/SectionPool.php @@ -62,6 +62,16 @@ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = return $sectionsData; } + /** + * Return array of section names. + * + * @return array + */ + public function getSectionNames() + { + return array_keys($this->sectionSourceMap); + } + /** * Get section sources by section names * diff --git a/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php b/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php index 5cd09aca9f873..d48ff5918c3f3 100644 --- a/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php +++ b/app/code/Magento/Customer/Helper/Session/CurrentCustomer.php @@ -10,7 +10,7 @@ use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\ViewInterface; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Framework\View\LayoutInterface; /** @@ -45,7 +45,7 @@ class CurrentCustomer protected $request; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 7be8699495bf5..985cfe0621bf8 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -978,6 +978,7 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); + $customer->setConfirmation(null); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); diff --git a/app/code/Magento/Customer/Model/Address/Validator/General.php b/app/code/Magento/Customer/Model/Address/Validator/General.php index 679f288712b4b..7cbb6ef1ab623 100644 --- a/app/code/Magento/Customer/Model/Address/Validator/General.php +++ b/app/code/Magento/Customer/Model/Address/Validator/General.php @@ -41,7 +41,7 @@ public function __construct( public function validate(AbstractAddress $address) { $errors = array_merge( - $this->checkRequredFields($address), + $this->checkRequiredFields($address), $this->checkOptionalFields($address) ); @@ -55,7 +55,7 @@ public function validate(AbstractAddress $address) * @return array * @throws \Zend_Validate_Exception */ - private function checkRequredFields(AbstractAddress $address) + private function checkRequiredFields(AbstractAddress $address) { $errors = []; if (!\Zend_Validate::is($address->getFirstname(), 'NotEmpty')) { diff --git a/app/code/Magento/Customer/Model/AddressSearchResults.php b/app/code/Magento/Customer/Model/AddressSearchResults.php new file mode 100644 index 0000000000000..7e83aa8f8d8df --- /dev/null +++ b/app/code/Magento/Customer/Model/AddressSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\AddressSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Address search results. + */ +class AddressSearchResults extends SearchResults implements AddressSearchResultsInterface +{ +} diff --git a/app/code/Magento/Customer/Model/AttributeMetadataConverter.php b/app/code/Magento/Customer/Model/AttributeMetadataConverter.php index 44d104659e069..0407aebf9d670 100644 --- a/app/code/Magento/Customer/Model/AttributeMetadataConverter.php +++ b/app/code/Magento/Customer/Model/AttributeMetadataConverter.php @@ -3,18 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model; use Magento\Customer\Api\Data\OptionInterfaceFactory; use Magento\Customer\Api\Data\ValidationRuleInterfaceFactory; use Magento\Customer\Api\Data\AttributeMetadataInterfaceFactory; use Magento\Eav\Api\Data\AttributeDefaultValueInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; /** * Converter for AttributeMetadata */ class AttributeMetadataConverter { + /** + * Attribute Code get options from system config + * + * @var array + */ + private const ATTRIBUTE_CODE_LIST_FROM_SYSTEM_CONFIG = ['prefix', 'suffix']; + + /** + * XML Path to get address config + * + * @var string + */ + private const XML_CUSTOMER_ADDRESS = 'customer/address/'; + /** * @var OptionInterfaceFactory */ @@ -35,6 +53,11 @@ class AttributeMetadataConverter */ protected $dataObjectHelper; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * Initialize the Converter * @@ -42,17 +65,20 @@ class AttributeMetadataConverter * @param ValidationRuleInterfaceFactory $validationRuleFactory * @param AttributeMetadataInterfaceFactory $attributeMetadataFactory * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param ScopeConfigInterface $scopeConfig */ public function __construct( OptionInterfaceFactory $optionFactory, ValidationRuleInterfaceFactory $validationRuleFactory, AttributeMetadataInterfaceFactory $attributeMetadataFactory, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + ScopeConfigInterface $scopeConfig = null ) { $this->optionFactory = $optionFactory; $this->validationRuleFactory = $validationRuleFactory; $this->attributeMetadataFactory = $attributeMetadataFactory; $this->dataObjectHelper = $dataObjectHelper; + $this->scopeConfig = $scopeConfig ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -64,28 +90,34 @@ public function __construct( public function createMetadataAttribute($attribute) { $options = []; - if ($attribute->usesSource()) { - foreach ($attribute->getSource()->getAllOptions() as $option) { - $optionDataObject = $this->optionFactory->create(); - if (!is_array($option['value'])) { - $optionDataObject->setValue($option['value']); - } else { - $optionArray = []; - foreach ($option['value'] as $optionArrayValues) { - $optionObject = $this->optionFactory->create(); - $this->dataObjectHelper->populateWithArray( - $optionObject, - $optionArrayValues, - \Magento\Customer\Api\Data\OptionInterface::class - ); - $optionArray[] = $optionObject; + + if (in_array($attribute->getAttributeCode(), self::ATTRIBUTE_CODE_LIST_FROM_SYSTEM_CONFIG)) { + $options = $this->getOptionFromConfig($attribute->getAttributeCode()); + } else { + if ($attribute->usesSource()) { + foreach ($attribute->getSource()->getAllOptions() as $option) { + $optionDataObject = $this->optionFactory->create(); + if (!is_array($option['value'])) { + $optionDataObject->setValue($option['value']); + } else { + $optionArray = []; + foreach ($option['value'] as $optionArrayValues) { + $optionObject = $this->optionFactory->create(); + $this->dataObjectHelper->populateWithArray( + $optionObject, + $optionArrayValues, + \Magento\Customer\Api\Data\OptionInterface::class + ); + $optionArray[] = $optionObject; + } + $optionDataObject->setOptions($optionArray); } - $optionDataObject->setOptions($optionArray); + $optionDataObject->setLabel($option['label']); + $options[] = $optionDataObject; } - $optionDataObject->setLabel($option['label']); - $options[] = $optionDataObject; } } + $validationRules = []; foreach ((array)$attribute->getValidateRules() as $name => $value) { $validationRule = $this->validationRuleFactory->create() @@ -122,4 +154,26 @@ public function createMetadataAttribute($attribute) ->setIsFilterableInGrid($attribute->getIsFilterableInGrid()) ->setIsSearchableInGrid($attribute->getIsSearchableInGrid()); } + + /** + * Get option from System Config instead of Use Source (Prefix, Suffix) + * + * @param string $attributeCode + * @return \Magento\Customer\Api\Data\OptionInterface[] + */ + private function getOptionFromConfig($attributeCode) + { + $result = []; + $value = $this->scopeConfig->getValue(self::XML_CUSTOMER_ADDRESS . $attributeCode . '_options'); + if ($value) { + $optionArray = explode(';', $value); + foreach ($optionArray as $value) { + $optionObject = $this->optionFactory->create(); + $optionObject->setLabel($value); + $optionObject->setValue($value); + $result[] = $optionObject; + } + } + return $result; + } } diff --git a/app/code/Magento/Customer/Model/AttributeMetadataResolver.php b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php index 979730eb1c9c2..c936de1bd0230 100644 --- a/app/code/Magento/Customer/Model/AttributeMetadataResolver.php +++ b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php @@ -16,6 +16,7 @@ use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\Share as ShareConfig; +use Magento\Customer\Model\FileUploaderDataResolver; /** * Class to build meta data of the customer or customer address attribute @@ -77,14 +78,14 @@ class AttributeMetadataResolver /** * @param CountryWithWebsites $countryWithWebsiteSource * @param EavValidationRules $eavValidationRules - * @param \Magento\Customer\Model\FileUploaderDataResolver $fileUploaderDataResolver + * @param FileUploaderDataResolver $fileUploaderDataResolver * @param ContextInterface $context * @param ShareConfig $shareConfig */ public function __construct( CountryWithWebsites $countryWithWebsiteSource, EavValidationRules $eavValidationRules, - fileUploaderDataResolver $fileUploaderDataResolver, + FileUploaderDataResolver $fileUploaderDataResolver, ContextInterface $context, ShareConfig $shareConfig ) { @@ -113,7 +114,12 @@ public function getAttributesMeta( // use getDataUsingMethod, since some getters are defined and apply additional processing of returning value foreach (self::$metaProperties as $metaName => $origName) { $value = $attribute->getDataUsingMethod($origName); - $meta['arguments']['data']['config'][$metaName] = ($metaName === 'label') ? __($value) : $value; + if ($metaName === 'label') { + $meta['arguments']['data']['config'][$metaName] = __($value); + $meta['arguments']['data']['config']['__disableTmpl'] = [$metaName => true]; + } else { + $meta['arguments']['data']['config'][$metaName] = $value; + } if ('frontend_input' === $origName) { $meta['arguments']['data']['config']['formElement'] = self::$formElement[$value] ?? $value; } @@ -124,7 +130,14 @@ public function getAttributesMeta( $meta['arguments']['data']['config']['options'] = $this->countryWithWebsiteSource ->getAllOptions(); } else { - $meta['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions(); + $options = $attribute->getSource()->getAllOptions(); + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = ['label' => true]; + } + ); + $meta['arguments']['data']['config']['options'] = $options; } } @@ -144,7 +157,6 @@ public function getAttributesMeta( $attribute, $meta['arguments']['data']['config'] ); - return $meta; } diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index 1287dbe5df708..2692d1edf0143 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -20,6 +20,7 @@ use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; /** * Customer model @@ -180,7 +181,7 @@ class Customer extends \Magento\Framework\Model\AbstractModel protected $_encryptor; /** - * @var \Magento\Framework\Math\Random + * @var Random */ protected $mathRandom; @@ -248,6 +249,7 @@ class Customer extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param AccountConfirmation|null $accountConfirmation + * @param Random|null $mathRandom * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -272,7 +274,8 @@ public function __construct( \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - AccountConfirmation $accountConfirmation = null + AccountConfirmation $accountConfirmation = null, + Random $mathRandom = null ) { $this->metadataService = $metadataService; $this->_scopeConfig = $scopeConfig; @@ -291,6 +294,7 @@ public function __construct( $this->indexerRegistry = $indexerRegistry; $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() ->get(AccountConfirmation::class); + $this->mathRandom = $mathRandom ?: ObjectManager::getInstance()->get(Random::class); parent::__construct( $context, $registry, @@ -394,7 +398,9 @@ public function getSharingConfig() public function authenticate($login, $password) { $this->loadByEmail($login); - if ($this->getConfirmation() && $this->isConfirmationRequired()) { + if ($this->getConfirmation() && + $this->accountConfirmation->isConfirmationRequired($this->getWebsiteId(), $this->getId(), $this->getEmail()) + ) { throw new EmailNotConfirmedException( __("This account isn't confirmed. Verify and try again.") ); @@ -415,8 +421,9 @@ public function authenticate($login, $password) /** * Load customer by email * - * @param string $customerEmail - * @return $this + * @param string $customerEmail + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function loadByEmail($customerEmail) { @@ -427,8 +434,9 @@ public function loadByEmail($customerEmail) /** * Change customer password * - * @param string $newPassword - * @return $this + * @param string $newPassword + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function changePassword($newPassword) { @@ -440,6 +448,7 @@ public function changePassword($newPassword) * Get full customer name * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function getName() { @@ -462,8 +471,9 @@ public function getName() /** * Add address to address collection * - * @param Address $address - * @return $this + * @param Address $address + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function addAddress(Address $address) { @@ -487,6 +497,7 @@ public function getAddressById($addressId) * * @param int $addressId * @return Address + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAddressItemById($addressId) { @@ -507,6 +518,7 @@ public function getAddressCollection() * Customer addresses collection * * @return \Magento\Customer\Model\ResourceModel\Address\Collection + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAddressesCollection() { @@ -538,6 +550,7 @@ public function getAddresses() * Retrieve all customer attributes * * @return Attribute[] + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAttributes() { @@ -592,6 +605,7 @@ public function hashPassword($password, $salt = true) * * @param string $password * @return boolean + * @throws \Exception */ public function validatePassword($password) { @@ -805,7 +819,7 @@ public function isConfirmationRequired() */ public function getRandomConfirmationKey() { - return md5(uniqid()); + return $this->mathRandom->getRandomString(32); } /** diff --git a/app/code/Magento/Customer/Model/Customer/Source/Group.php b/app/code/Magento/Customer/Model/Customer/Source/Group.php index efcc7d0fe93a4..1064152b20fd5 100644 --- a/app/code/Magento/Customer/Model/Customer/Source/Group.php +++ b/app/code/Magento/Customer/Model/Customer/Source/Group.php @@ -6,7 +6,7 @@ namespace Magento\Customer\Model\Customer\Source; use Magento\Customer\Api\Data\GroupSearchResultsInterface; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; diff --git a/app/code/Magento/Customer/Model/CustomerSearchResults.php b/app/code/Magento/Customer/Model/CustomerSearchResults.php new file mode 100644 index 0000000000000..1d7f0e265641f --- /dev/null +++ b/app/code/Magento/Customer/Model/CustomerSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\CustomerSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Customer search results. + */ +class CustomerSearchResults extends SearchResults implements CustomerSearchResultsInterface +{ +} diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 573f86247e0c3..432317444f4b7 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -340,7 +340,7 @@ public function passwordReminder(CustomerInterface $customer) */ public function passwordResetConfirmation(CustomerInterface $customer) { - $storeId = $this->storeManager->getStore()->getId(); + $storeId = $customer->getStoreId(); if (!$storeId) { $storeId = $this->getWebsiteStoreId($customer); } diff --git a/app/code/Magento/Customer/Model/GroupSearchResults.php b/app/code/Magento/Customer/Model/GroupSearchResults.php new file mode 100644 index 0000000000000..1de4cd078db88 --- /dev/null +++ b/app/code/Magento/Customer/Model/GroupSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\GroupSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Customer Groups search results. + */ +class GroupSearchResults extends SearchResults implements GroupSearchResultsInterface +{ +} diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php index 517aef5690ee6..577c97a19268a 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php @@ -15,6 +15,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Psr\Log\LoggerInterface; +/** + * Plugin before \Magento\Framework\App\Action\AbstractAction::dispatch. + * + * Plugin to remove notifications from cache. + */ class CustomerNotification { /** @@ -66,6 +71,8 @@ public function __construct( } /** + * Removes notifications from cache. + * * @param AbstractAction $subject * @param RequestInterface $request * @return void @@ -82,10 +89,10 @@ public function beforeDispatch(AbstractAction $subject, RequestInterface $reques ) ) { try { + $this->session->regenerateId(); $customer = $this->customerRepository->getById($customerId); $this->session->setCustomerData($customer); $this->session->setCustomerGroupId($customer->getGroupId()); - $this->session->regenerateId(); $this->notificationStorage->remove(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customer->getId()); } catch (NoSuchEntityException $e) { $this->logger->error($e); diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index 94196df6fe093..1477287f79f4b 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Model\ResourceModel; +use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Framework\App\ObjectManager; use Magento\Framework\Validator\Exception as ValidatorException; @@ -42,12 +43,19 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity */ protected $storeManager; + /** + * @var AccountConfirmation + */ + private $accountConfirmation; + /** * @var NotificationStorage */ private $notificationStorage; /** + * Customer constructor. + * * @param \Magento\Eav\Model\Entity\Context $context * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite @@ -56,6 +64,7 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $data + * @param AccountConfirmation $accountConfirmation */ public function __construct( \Magento\Eav\Model\Entity\Context $context, @@ -65,15 +74,19 @@ public function __construct( \Magento\Framework\Validator\Factory $validatorFactory, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Store\Model\StoreManagerInterface $storeManager, - $data = [] + $data = [], + AccountConfirmation $accountConfirmation = null ) { parent::__construct($context, $entitySnapshot, $entityRelationComposite, $data); + $this->_scopeConfig = $scopeConfig; $this->_validatorFactory = $validatorFactory; $this->dateTime = $dateTime; - $this->storeManager = $storeManager; + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); $this->setType('customer'); $this->setConnection('customer_read', 'customer_write'); + $this->storeManager = $storeManager; } /** @@ -144,7 +157,13 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) } // set confirmation key logic - if (!$customer->getId() && $customer->isConfirmationRequired()) { + if (!$customer->getId() && + $this->accountConfirmation->isConfirmationRequired( + $customer->getWebsiteId(), + $customer->getId(), + $customer->getEmail() + ) + ) { $customer->setConfirmation($customer->getRandomConfirmationKey()); } // remove customer confirmation key from database, if empty diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 529b0e806972a..03cf4b1bdddec 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -7,24 +7,25 @@ namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; -use Magento\Framework\Api\ExtensibleDataObjectConverter; -use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Customer\Model\Customer as CustomerModel; +use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\Data\CustomerSecureFactory; -use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Customer\Model\Delegation\Data\NewOperation; -use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Api\ImageProcessorInterface; +use Magento\Framework\Api\Search\FilterGroup; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; -use Magento\Framework\Api\Search\FilterGroup; -use Magento\Framework\Event\ManagerInterface; -use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Event\ManagerInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -203,7 +204,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) $customer->setAddresses([]); $customerData = $this->extensibleDataObjectConverter->toNestedArray($customer, [], CustomerInterface::class); $customer->setAddresses($origAddresses); - /** @var Customer $customerModel */ + /** @var CustomerModel $customerModel */ $customerModel = $this->customerFactory->create(['data' => $customerData]); //Model's actual ID field maybe different than "id" so "id" field from $customerData may be ignored. $customerModel->setId($customer->getId()); diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php index 5900fed218edf..e9dc7700ec090 100644 --- a/app/code/Magento/Customer/Model/Session.php +++ b/app/code/Magento/Customer/Model/Session.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Model\Config\Share; use Magento\Customer\Model\ResourceModel\Customer as ResourceCustomer; +use Magento\Framework\App\ObjectManager; /** * Customer session model @@ -17,6 +18,7 @@ * @api * @method string getNoReferer() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Session extends \Magento\Framework\Session\SessionManager @@ -107,6 +109,8 @@ class Session extends \Magento\Framework\Session\SessionManager protected $response; /** + * Session constructor. + * * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver * @param \Magento\Framework\Session\Config\ConfigInterface $sessionConfig @@ -118,7 +122,7 @@ class Session extends \Magento\Framework\Session\SessionManager * @param \Magento\Framework\App\State $appState * @param Share $configShare * @param \Magento\Framework\Url\Helper\Data $coreUrl - * @param \Magento\Customer\Model\Url $customerUrl + * @param Url $customerUrl * @param ResourceCustomer $customerResource * @param CustomerFactory $customerFactory * @param \Magento\Framework\UrlFactory $urlFactory @@ -128,6 +132,7 @@ class Session extends \Magento\Framework\Session\SessionManager * @param CustomerRepositoryInterface $customerRepository * @param GroupManagementInterface $groupManagement * @param \Magento\Framework\App\Response\Http $response + * @param AccountConfirmation $accountConfirmation * @throws \Magento\Framework\Exception\SessionException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -152,7 +157,8 @@ public function __construct( \Magento\Framework\App\Http\Context $httpContext, CustomerRepositoryInterface $customerRepository, GroupManagementInterface $groupManagement, - \Magento\Framework\App\Response\Http $response + \Magento\Framework\App\Response\Http $response, + AccountConfirmation $accountConfirmation = null ) { $this->_coreUrl = $coreUrl; $this->_customerUrl = $customerUrl; @@ -177,6 +183,8 @@ public function __construct( ); $this->groupManagement = $groupManagement; $this->response = $response; + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); $this->_eventManager->dispatch('customer_session_init', ['customer_session' => $this]); } @@ -216,6 +224,8 @@ public function setCustomerData(CustomerData $customer) * Retrieve customer model object * * @return CustomerData + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCustomerData() { @@ -266,8 +276,14 @@ public function setCustomer(Customer $customerModel) \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID ); $this->setCustomerId($customerModel->getId()); - if (!$customerModel->isConfirmationRequired() && $customerModel->getConfirmation()) { - $customerModel->setConfirmation(null)->save(); + $accountConfirmationRequired = $this->accountConfirmation->isConfirmationRequired( + $customerModel->getWebsiteId(), + $customerModel->getId(), + $customerModel->getEmail() + ); + if (!$accountConfirmationRequired && $customerModel->getConfirmation() && $customerModel->getId()) { + $customerModel->setConfirmation(null); + $this->_customerResource->save($customerModel); } /** @@ -354,10 +370,13 @@ public function setCustomerGroupId($id) } /** - * Get customer group id - * If customer is not logged in system, 'not logged in' group id will be returned + * Get customer group id. + * + * If customer is not logged in system, 'not logged in' group id will be returned. * * @return int + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCustomerGroupId() { @@ -407,24 +426,29 @@ public function checkCustomerId($customerId) } /** + * Sets customer as logged in + * * @param Customer $customer * @return $this */ public function setCustomerAsLoggedIn($customer) { + $this->regenerateId(); $this->setCustomer($customer); $this->_eventManager->dispatch('customer_login', ['customer' => $customer]); $this->_eventManager->dispatch('customer_data_object_login', ['customer' => $this->getCustomerDataObject()]); - $this->regenerateId(); return $this; } /** + * Sets customer as logged in + * * @param CustomerData $customer * @return $this */ public function setCustomerDataAsLoggedIn($customer) { + $this->regenerateId(); $this->_httpContext->setValue(Context::CONTEXT_AUTH, true, false); $this->setCustomerData($customer); @@ -521,6 +545,8 @@ protected function _setAuthUrl($key, $url) * Logout without dispatching event * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _logout() { @@ -567,6 +593,8 @@ public function regenerateId() } /** + * Creates URL object + * * @return \Magento\Framework\UrlInterface */ protected function _createUrl() diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php index 4f129f05aa82c..6935b9dca7f28 100644 --- a/app/code/Magento/Customer/Model/Visitor.php +++ b/app/code/Magento/Customer/Model/Visitor.php @@ -169,6 +169,11 @@ public function initByRequest($observer) $this->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)); + // prevent saving Visitor for safe methods, e.g. GET request + if ($this->requestSafety->isSafeMethod()) { + return $this; + } + if (!$this->getId()) { $this->setSessionId($this->session->getSessionId()); $this->save(); @@ -260,7 +265,7 @@ public function bindCustomerLogout($observer) * Create binding of checkout quote * * @param \Magento\Framework\Event\Observer $observer - * @return \Magento\Customer\Model\Visitor + * @return \Magento\Customer\Model\Visitor */ public function bindQuoteCreate($observer) { @@ -278,7 +283,7 @@ public function bindQuoteCreate($observer) * Destroy binding of checkout quote * * @param \Magento\Framework\Event\Observer $observer - * @return \Magento\Customer\Model\Visitor + * @return \Magento\Customer\Model\Visitor */ public function bindQuoteDestroy($observer) { diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertDefaultValueDisableAutoGroupInCustomerFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertDefaultValueDisableAutoGroupInCustomerFormActionGroup.xml new file mode 100644 index 0000000000000..8271cdec46df9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertDefaultValueDisableAutoGroupInCustomerFormActionGroup.xml @@ -0,0 +1,26 @@ +<?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"> + <!-- Check Default Value for Disable Automatic Group Changes Based on VAT ID in Customer Form --> + <actionGroup name="AdminAssertDefaultValueDisableAutoGroupInCustomerFormActionGroup"> + <annotations> + <description>Check Default Value for Disable Automatic Group Changes Based on VAT ID in Create Customer form.</description> + </annotations> + <arguments> + <argument name="isChecked" type="string"/> + </arguments> + + <grabValueFrom selector="{{AdminCustomerAccountInformationSection.disableAutomaticGroupChange}}" stepKey="grabDisableAutomaticGroupChange"/> + <assertEquals stepKey="assertDisableAutomaticGroupChangeNo" message="pass"> + <expectedResult type="string">{{isChecked}}</expectedResult> + <actualResult type="variable">grabDisableAutomaticGroupChange</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml index 5eb52630d906b..36b41b155b2b3 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertErrorMessageCustomerGroupAlreadyExistsActionGroup.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminAssertErrorMessageCustomerGroupAlreadyExists" extends="AdminCreateCustomerGroupActionGroup"> <remove keyForRemoval="seeCustomerGroupSaveMessage"/> - <waitForElementVisible selector="{{AdminMessagesSection.errorMessage}}" stepKey="waitForElementVisible"/> - <see selector="{{AdminMessagesSection.errorMessage}}" userInput="Customer Group already exists." stepKey="seeErrorMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.error}}" stepKey="waitForElementVisible"/> + <see selector="{{AdminMessagesSection.error}}" userInput="Customer Group already exists." stepKey="seeErrorMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml index ab5ae53fd4caa..8171b97258157 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="customerGroupName" type="string"/> </arguments> - + <amOnPage url="{{AdminCustomerGroupsIndexPage.url}}" stepKey="goToAdminCustomerGroupIndexPage"/> <waitForPageLoad time="30" stepKey="waitForCustomerGroupIndexPageLoad"/> <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> @@ -24,6 +24,8 @@ <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> <click selector="{{AdminCustomerGroupGridActionsSection.selectButton(customerGroupName)}}" stepKey="clickSelectButton"/> <click selector="{{AdminCustomerGroupGridActionsSection.deleteAction(customerGroupName)}}" stepKey="clickOnDeleteItem"/> + <waitForElementVisible selector="{{AdminGridConfirmActionSection.message}}" stepKey="waitForConfirmModal"/> + <see selector="{{AdminGridConfirmActionSection.message}}" userInput="Are you sure you want to delete a {{customerGroupName}} record?" stepKey="seeRemoveMessage"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDeleteCustomerGroup"/> <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateNewCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateNewCustomerActionGroup.xml new file mode 100644 index 0000000000000..81c788fc4445a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateNewCustomerActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AdminNavigateNewCustomerActionGroup"> + <annotations> + <description>Goes to the New Customer page.</description> + </annotations> + + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml index e47aa8809f080..62c35dd230f10 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAddressActionGroup.xml @@ -9,6 +9,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminSaveCustomerAddressActionGroup"> <click selector="{{StorefrontCustomerAddressFormSection.saveAddress}}" stepKey="saveCustomerAddress"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You saved the address." stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGenderInCustomersGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGenderInCustomersGridActionGroup.xml new file mode 100644 index 0000000000000..0b51140471ab7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGenderInCustomersGridActionGroup.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="AdminUpdateCustomerGenderInCustomersGridActionGroup"> + <annotations> + <description>Update customer gender attribute value on customers grid page</description> + </annotations> + <arguments> + <argument name="customerEmail" defaultValue="{{Simple_US_Customer.email}}" type="string"/> + <argument name="genderValue" defaultValue="{{Gender.empty}}" type="string"/> + </arguments> + + <click selector="{{AdminDataGridTableSection.rowTemplate(customerEmail)}}" stepKey="clickCustomersGridRow"/> + <waitForElementVisible selector="{{AdminCustomerGridInlineEditorSection.customerGenderEditor}}" stepKey="waitForGenderElementAppears"/> + <selectOption userInput="{{genderValue}}" selector="{{AdminCustomerGridInlineEditorSection.customerGenderEditor}}" stepKey="selectGenderValue"/> + <click selector="{{AdminCustomerGridInlineEditorSection.saveInGrid}}" stepKey="saveCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerGenderInCustomersGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerGenderInCustomersGridActionGroup.xml new file mode 100644 index 0000000000000..8b73a9c3307e3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerGenderInCustomersGridActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertAdminCustomerGenderInCustomersGridActionGroup"> + <annotations> + <description>Assert customer "Gender" attribute value on customer grid page</description> + </annotations> + <arguments> + <argument name="customerEmail" defaultValue="{{Simple_US_Customer.email}}" type="string"/> + <argument name="expectedGenderValue" defaultValue="{{Gender.empty}}" type="string"/> + </arguments> + + <see userInput="{{expectedGenderValue}}" selector="{{AdminCustomerGridSection.customerGenderByEmail(customerEmail)}}" stepKey="assertGenderValue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerGenderOnCustomerFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerGenderOnCustomerFormActionGroup.xml new file mode 100644 index 0000000000000..b21b0054b0c78 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerGenderOnCustomerFormActionGroup.xml @@ -0,0 +1,23 @@ +<?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="AssertAdminCustomerGenderOnCustomerFormActionGroup"> + <annotations> + <description>Validates that the provided Customer Gender is selected on the Admin Customer edit page.</description> + </annotations> + <arguments> + <argument name="customerGender" defaultValue="{{Gender.empty}}" type="string"/> + </arguments> + + <conditionalClick selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" dependentSelector="{{AdminCustomerAccountInformationSection.gender}}" visible="false" stepKey="clickOnAccountInfoTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeOptionIsSelected userInput="{{customerGender}}" selector="{{AdminCustomerAccountInformationSection.gender}}" stepKey="verifyNeededCustomerGenderSelected"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml index 60a8a49954bab..e338d1ae4bbd0 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="customer"/> </arguments> - + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> @@ -26,7 +26,7 @@ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEdit"/> <waitForPageLoad stepKey="waitForPageLoad3"/> </actionGroup> - + <actionGroup name="OpenEditCustomerAddressFromAdminActionGroup"> <annotations> <description>Filters the Admin Customers Addresses based on the provided Address. Clicks on Edit.</description> @@ -34,7 +34,7 @@ <arguments> <argument name="address"/> </arguments> - + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="openAddressesTab"/> <waitForElementVisible selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="waitForComponentLoad"/> <click selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="openAddressesFilter"/> @@ -67,7 +67,7 @@ <click selector="{{AdminCustomerGridMainActionsSection.delete}}" stepKey="clickDelete"/> <waitForAjaxLoad stepKey="waitForLoadConfirmation"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="A total of 1 record(s) were deleted" stepKey="seeSuccess"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) were deleted" stepKey="seeSuccess"/> </actionGroup> <actionGroup name="AdminClearCustomersFiltersActionGroup"> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StoreFrontClickEditDefaultShippingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StoreFrontClickEditDefaultShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..36c62a887c180 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StoreFrontClickEditDefaultShippingAddressActionGroup.xml @@ -0,0 +1,19 @@ +<?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="StoreFrontClickEditDefaultShippingAddressActionGroup"> + <annotations> + <description>Click on the edit default shipping address link.</description> + </annotations> + + <click stepKey="ClickEditDefaultShippingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultShippingAddress}}"/> + <waitForPageLoad stepKey="waitForStorefrontSignInPageLoad"/> +</actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerElementNotVisibleActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerElementNotVisibleActionGroup.xml new file mode 100644 index 0000000000000..12a0b8f47c5fa --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerElementNotVisibleActionGroup.xml @@ -0,0 +1,13 @@ +<?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="StorefrontCustomerReorderButtonNotVisibleActionGroup"> + <dontSeeElement selector="{{StorefrontCustomerOrderViewSection.reorder}}" stepKey="assertNotVisibleElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontNavigateToCustomerOrdersHistoryPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontNavigateToCustomerOrdersHistoryPageActionGroup.xml new file mode 100644 index 0000000000000..40a79b87b01a9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontNavigateToCustomerOrdersHistoryPageActionGroup.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="StorefrontNavigateToCustomerOrdersHistoryPageActionGroup"> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="amOnTheCustomerPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml index b013b1db1c8e7..31a988ac9da0d 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml @@ -16,4 +16,15 @@ <amOnPage url="{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> <waitForPageLoad stepKey="waitForPageLoaded"/> </actionGroup> + + <actionGroup name="StorefrontOpenCustomerAccountCreatePageUsingStoreCodeInUrlActionGroup"> + <annotations> + <description>Goes to the Storefront Customer Create page using Store code in URL option.</description> + </annotations> + <arguments> + <argument name="storeView" type="string" defaultValue="{{customStore.code}}"/> + </arguments> + + <amOnPage url="{{StorefrontStoreHomePage.url(storeView)}}{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index 4d7a39b3246e1..08e13885d10d4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -51,6 +51,21 @@ <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionTX</requiredEntity> </entity> + <entity name="US_Address_TX_Without_Default" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> <entity name="US_Address_TX_Default_Billing" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -239,6 +254,21 @@ <data key="state">Côtes-d'Armor</data> <data key="postcode">12345</data> </entity> + <entity name="updateCustomerChinaAddress" type="address"> + <data key="firstname">Xian</data> + <data key="lastname">Shai</data> + <data key="company">Hunan Fenmian</data> + <data key="telephone">+86 851 8410 4337</data> + <array key="street"> + <item>Nanyuan Rd, Wudang</item> + <item>Hunan Fenmian</item> + </array> + <data key="country_id">CN</data> + <data key="country">China</data> + <data key="city">Guiyang</data> + <data key="state">Guizhou Sheng</data> + <data key="postcode">550002</data> + </entity> <entity name="updateCustomerNoXSSInjection" type="address"> <data key="firstname">Jany</data> <data key="lastname">Doe</data> @@ -262,7 +292,7 @@ <item>Piwowarska 6</item> </array> <data key="city">Bielsko-Biała</data> - <data key="state"> Bielsko</data> + <data key="state">śląskie</data> <data key="country_id">PL</data> <data key="country">Poland</data> <data key="postcode">43-310</data> @@ -312,4 +342,37 @@ <data key="postcode">90230</data> <data key="telephone">555-55-555-55</data> </entity> + <entity name="US_Address_AE" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + <item>113</item> + </array> + <data key="city">Los Angeles</data> + <data key="state">Armed Forces Europe</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">90001</data> + <data key="telephone">512-345-6789</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionAE</requiredEntity> + </entity> + <entity name="updateCustomerBelgiumAddress" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>Chaussee de Wavre</item> + <item>318</item> + </array> + <data key="city">Bihain</data> + <data key="state">Hainaut</data> + <data key="country_id">BE</data> + <data key="country">Belgium</data> + <data key="postcode">6690</data> + <data key="telephone">0477-58-77867</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml new file mode 100644 index 0000000000000..e4c020cc449fb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml @@ -0,0 +1,15 @@ +<?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="AdminGeneralSetVatNumberConfigData"> + <data key="path">general/store_information/merchant_vat_number</data> + <data key="value">111607872</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml index ab4307082595d..3eb14604220e9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml @@ -51,4 +51,14 @@ <data key="label">5</data> <data key="value">5</data> </entity> + <entity name="CustomerAccountShareWebsiteConfigData"> + <data key="path">customer/account_share/scope</data> + <data key="label">Per Website</data> + <data key="value">1</data> + </entity> + <entity name="CustomerAccountShareGlobalConfigData"> + <data key="path">customer/account_share/scope</data> + <data key="label">Global</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index c7a73b61dc48a..093d6a05e8c5c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -47,6 +47,20 @@ <data key="group">General</data> <requiredEntity type="address">US_Address_TX</requiredEntity> </entity> + <entity name="Simple_US_Customer_Without_Default_Address" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <data key="group">General</data> + <requiredEntity type="address">US_Address_TX_Without_Default</requiredEntity> + </entity> <entity name="SimpleUsCustomerWithNewCustomerGroup" type="customer"> <data key="default_billing">true</data> <data key="default_shipping">true</data> @@ -309,4 +323,17 @@ <data key="store_id">0</data> <data key="website_id">0</data> </entity> + <entity name="Simple_US_Customer_ArmedForcesEurope" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_AE</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml index 28305d37cf77b..68e4090d64910 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -30,7 +30,7 @@ </array> </entity> <entity name="CustomCustomerGroup" type="customerGroup"> - <data key="code" unique="suffix">Group </data> + <data key="code" unique="suffix">Group-</data> <data key="tax_class_id">3</data> <data key="tax_class_name">Retail Customer</data> </entity> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/GenderData.xml b/app/code/Magento/Customer/Test/Mftf/Data/GenderData.xml new file mode 100644 index 0000000000000..68dee0ffa31d0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/GenderData.xml @@ -0,0 +1,17 @@ +<?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="Gender" type="gender"> + <data key="empty"/> + <data key="male">Male</data> + <data key="female">Female</data> + <data key="not_specified">Not Specified</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml index 280bae7de411a..0a956f16767be 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml @@ -32,4 +32,9 @@ <data key="region_code">UT</data> <data key="region_id">58</data> </entity> + <entity name="RegionAE" type="region"> + <data key="region">Armed Forces Europe</data> + <data key="region_code">AFE</data> + <data key="region_id">9</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml index 114c737e361ed..d77dc15840e4c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml @@ -13,5 +13,6 @@ <section name="AdminCustomerMessagesSection"/> <section name="AdminCustomerGridSection"/> <section name="AdminCustomerFiltersSection"/> + <section name="AdminCustomerGridInlineEditorSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml index 9bd382da8eb92..79fb18afaad53 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml @@ -12,6 +12,8 @@ <section name="AdminCustomerAddressesGridSection"/> <section name="AdminCustomerAddressesGridActionsSection"/> <section name="AdminCustomerAddressesSection"/> + <section name="AdminCustomerCartSection" /> + <section name="AdminCustomerInformationSection" /> <section name="AdminCustomerMainActionsSection"/> <section name="AdminEditCustomerAddressesSection" /> </page> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml index 4b36486f0bd17..2c9e66c15bbab 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml @@ -17,6 +17,7 @@ <element name="firstName" type="input" selector="input[name='customer[firstname]']"/> <element name="lastName" type="input" selector="input[name='customer[lastname]']"/> <element name="email" type="input" selector="input[name='customer[email]']"/> + <element name="disableAutomaticGroupChange" type="input" selector="input[name='customer[disable_auto_group_change]']"/> <element name="group" type="select" selector="[name='customer[group_id]']"/> <element name="groupIdValue" type="text" selector="//*[@name='customer[group_id]']/option"/> <element name="groupValue" type="button" selector="//span[text()='{{groupValue}}']" parameterized="true"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml index e743c4af66d9f..3ecbf5ff450c8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml @@ -17,5 +17,6 @@ <element name="filters" type="button" selector="button[data-action='grid-filter-expand']" timeout="30"/> <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="30"/> <element name="headerRow" type="text" selector=".admin__data-grid-header-row.row.row-gutter"/> + <element name="clearFilter" type="button" selector=".admin__data-grid-header .admin__data-grid-filters-current button.action-clear" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml new file mode 100644 index 0000000000000..5c8b8907db43a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.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="AdminCustomerCartSection"> + <element name="cartItem" type="button" selector="#customer_cart_grid_table tbody tr:nth-of-type({{row}}) .col-product_id" parameterized="true" timeout="5"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.xml index a01687990999e..6b0cafe8dc00b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCreateNewOrderSection.xml @@ -12,7 +12,7 @@ <element name="updateChangesBtn" type="button" selector=".order-sidebar .actions .action-default.scalable" timeout="30"/> <element name="productName" type="text" selector="#order-items_grid span[id*=order_item]"/> <element name="productPrice" type="text" selector=".even td[class=col-price] span[class=price]"/> - <element name="productQty" type="input" selector="td[class=col-qty] input"/> + <element name="productQty" type="input" selector="td[class=col-qty] .input-text.item-qty.admin__control-text"/> <element name="gridCell" type="text" selector="//div[contains(@id, 'order-items_grid')]//tbody[{{row}}]//td[count(//table[contains(@class, 'order-tables')]//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridInlineEditorSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridInlineEditorSection.xml new file mode 100644 index 0000000000000..d010844cfffcf --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridInlineEditorSection.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="AdminCustomerGridInlineEditorSection"> + <element name="customerGenderEditor" type="select" selector="tr.data-grid-editable-row:not([style*='display: none']) [name='gender']"/> + <element name="saveInGrid" type="button" selector="tr.data-grid-editable-row-actions button.action-primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml index 91363c614c1f8..9562a902b26da 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml @@ -16,5 +16,7 @@ <element name="customerCheckboxByEmail" type="checkbox" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//input[@type='checkbox']" parameterized="true" timeout="30"/> <element name="customerEditLinkByEmail" type="text" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//a[@class='action-menu-item']" parameterized="true" timeout="30"/> <element name="customerGroupByEmail" type="text" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//div[text()='{{customerGroup}}']" parameterized="true"/> + <element name="customerGenderByEmail" type="text" selector="//tr[@class='data-row']//div[text()='{{customerEmail}}']/ancestor::tr/td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Gender')]/preceding-sibling::th) +1]" parameterized="true"/> </section> </sections> + diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml new file mode 100644 index 0000000000000..d680015230b9d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml @@ -0,0 +1,22 @@ +<?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="AdminCustomerInformationSection"> + <element name="customerView" type="button" selector="#tab_customer_edit_tab_view_content"/> + <element name="accountInformation" type="button" selector="#tab_customer"/> + <element name="addresses" type="button" selector="#tab_address"/> + <element name="orders" type="button" selector="#tab_orders_content"/> + <element name="shoppingCart" type="button" selector="#tab_cart_content"/> + <element name="newsletter" type="button" selector="#tab_newsletter_content"/> + <element name="billingAgreements" type="button" selector="#tab_customer_edit_tab_agreements_content"/> + <element name="productReviews" type="button" selector="#tab_reviews_content"/> + <element name="wishList" type="button" selector="#tab_wishlist_content"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml new file mode 100644 index 0000000000000..7ca9a6993f2fc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml @@ -0,0 +1,83 @@ +<?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="AdminChangeCustomerGenderInCustomersGridTest"> + <annotations> + <features value="Customer"/> + <stories value="Update Customer"/> + <title value="Gender attribute blank value is saved in direct edits from customer grid"/> + <description value="Check that gender attribute blank value can be saved on customers grid"/> + <severity value="MAJOR"/> + <testCaseId value="MC-22025"/> + <useCaseId value="MC-17259"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Reset customer grid filter --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGridPage"/> + <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup ref="AdminResetFilterInCustomerGrid" stepKey="resetFilter"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Open customers grid page, filter by created customer--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCustomerGridByEmail"> + <argument name="email" value="$createCustomer.email$"/> + </actionGroup> + <!-- Check customer is in grid--> + <actionGroup ref="AdminAssertCustomerInCustomersGrid" stepKey="assertCustomerInCustomersGrid"> + <argument name="text" value="$createCustomer.email$"/> + <argument name="row" value="1"/> + </actionGroup> + <!--Check customer Gender value in grid--> + <actionGroup ref="AssertAdminCustomerGenderInCustomersGridActionGroup" stepKey="assertCustomerGenderInCustomersGrid"> + <argument name="customerEmail" value="$createCustomer.email$"/> + </actionGroup> + <!--Update customer Gender to Male--> + <actionGroup ref="AdminUpdateCustomerGenderInCustomersGridActionGroup" stepKey="updateCustomerGenderWithMaleValueInCustomersGrid"> + <argument name="customerEmail" value="$createCustomer.email$"/> + <argument name="genderValue" value="{{Gender.male}}"/> + </actionGroup> + <!--Check customer Gender value in grid--> + <actionGroup ref="AssertAdminCustomerGenderInCustomersGridActionGroup" stepKey="assertCustomerGenderMaleInCustomersGrid"> + <argument name="customerEmail" value="$createCustomer.email$"/> + <argument name="expectedGenderValue" value="{{Gender.male}}"/> + </actionGroup> + <!--Open customer edit page and check Gender value--> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPageWithMaleGender"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <actionGroup ref="AssertAdminCustomerGenderOnCustomerFormActionGroup" stepKey="assertCustomerGenderValueIsMaleOnCustomerForm"> + <argument name="customerGender" value="{{Gender.male}}"/> + </actionGroup> + <!--Filter customers grid by email--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCustomerByEmailToUpdateWithEmptyGender"> + <argument name="email" value="$createCustomer.email$"/> + </actionGroup> + <!--Update customer Gender to empty value--> + <actionGroup ref="AdminUpdateCustomerGenderInCustomersGridActionGroup" stepKey="updateCustomerGenderWithEmptyValueInCustomersGrid"> + <argument name="customerEmail" value="$createCustomer.email$"/> + </actionGroup> + <!--Check customer Gender value in grid--> + <actionGroup ref="AssertAdminCustomerGenderInCustomersGridActionGroup" stepKey="assertCustomerGenderEmptyInCustomersGrid"> + <argument name="customerEmail" value="$createCustomer.email$"/> + </actionGroup> + <!--Open customer edit page and check Gender value--> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPageWithEmptyGender"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <actionGroup ref="AssertAdminCustomerGenderOnCustomerFormActionGroup" stepKey="assertCustomerGenderValueIsEmptyOnCustomerForm"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml new file mode 100644 index 0000000000000..be96765920bf5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest"> + <annotations> + <features value="Customer"/> + <stories value="Default Value for Disable Automatic Group Changes Based on VAT ID"/> + <title value="Check settings Default Value for Disable Automatic Group Changes Based on VAT ID is No"/> + <description value="Check settings Default Value for Disable Automatic Group Changes Based on VAT ID is No"/> + <severity value="MAJOR"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + </before> + <after> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomer"/> + + <actionGroup ref="AdminAssertDefaultValueDisableAutoGroupInCustomerFormActionGroup" stepKey="seeDefaultValueInForm"> + <argument name="isChecked" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml new file mode 100644 index 0000000000000..87cba0c10dbc1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml @@ -0,0 +1,38 @@ +<?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="AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest"> + <annotations> + <features value="Customer"/> + <stories value="Default Value for Disable Automatic Group Changes Based on VAT ID"/> + <title value="Check settings Default Value for Disable Automatic Group Changes Based on VAT ID is Yes"/> + <description value="Check settings Default Value for Disable Automatic Group Changes Based on VAT ID is Yes"/> + <severity value="MAJOR"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 1" stepKey="setConfigDefaultIsYes"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + </before> + <after> + <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomer"/> + + <actionGroup ref="AdminAssertDefaultValueDisableAutoGroupInCustomerFormActionGroup" stepKey="seeDefaultValueInForm"> + <argument name="isChecked" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 78bae7ad60dd8..a11fb9d0eaa8f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -23,7 +23,7 @@ <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml index cbc8b89d3f242..d2435a093046a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml @@ -34,7 +34,7 @@ <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> - <!--Add the Address --> + <!-- Add the Address --> <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> <waitForPageLoad stepKey="waitForAddressPageToLoad"/> <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> @@ -44,6 +44,7 @@ <fillField selector="{{AdminEditCustomerAddressesSection.city}}" userInput="{{PolandAddress.city}}" stepKey="fillCity"/> <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{PolandAddress.country}}" stepKey="fillCountry"/> + <selectOption selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{PolandAddress.state}}" stepKey="fillState"/> <fillField selector="{{AdminEditCustomerAddressesSection.zipCode}}" userInput="{{PolandAddress.postcode}}" stepKey="fillPostCode"/> <fillField selector="{{AdminEditCustomerAddressesSection.phone}}" userInput="{{PolandAddress.telephone}}" stepKey="fillPhoneNumber"/> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> @@ -60,6 +61,7 @@ <see userInput="$$createCustomer.firstname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> <see userInput="$$createCustomer.lastname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> <see userInput="$$createCustomer.email$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + <see userInput="{{PolandAddress.state}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertState"/> <see userInput="{{PolandAddress.country}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCountry"/> <see userInput="{{PolandAddress.postcode}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPostCode"/> <see userInput="{{PolandAddress.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> @@ -86,6 +88,7 @@ <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.street}}" stepKey="seeStreetAddress"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.city}}" stepKey="seeCity"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.country}}" stepKey="seeCountry"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.state}}" stepKey="seeState"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.postcode}}" stepKey="seePostCode"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.telephone}}" stepKey="seePhoneNumber"/> </test> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml index 43f2aa7f8de95..a487571c43534 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml @@ -85,6 +85,7 @@ <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.telephone}}" stepKey="seePhoneNumberInDefaultAddressSection"/> <!--Assert Customer Address Grid --> + <conditionalClick selector="{{AdminCustomerAddressesGridActionsSection.clearFilter}}" dependentSelector="{{AdminCustomerAddressesGridActionsSection.clearFilter}}" visible="true" stepKey="clearAddressesGridFilter"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.street}}" stepKey="seeStreetAddress"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.city}}" stepKey="seeCity"/> <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.country}}" stepKey="seeCountry"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml new file mode 100644 index 0000000000000..d2d3343a3b8d3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml @@ -0,0 +1,75 @@ +<?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="AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Order"/> + <title value="Place an order and click print"/> + <description value="Admin panel is not frozen if Storefront is opened via Customer View"/> + <severity value="MAJOR"/> + <testCaseId value="https://github.com/magento/magento2/pull/24845"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="simpleCustomer"/> + <createData entity="SimpleSubCategory" stepKey="createSimpleCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createSimpleCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderPage"> + <argument name="customer" value="$simpleCustomer$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSecondProduct"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerInfo"> + <argument name="customer" value="$simpleCustomer$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRate"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> + + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipment"/> + <actionGroup ref="submitShipmentIntoOrder" stepKey="submitShipment"/> + + <!--Create Credit Memo--> + <actionGroup ref="StartToCreateCreditMemoActionGroup" stepKey="startToCreateCreditMemo"> + <argument name="orderId" value="{$getOrderId}"/> + </actionGroup> + <actionGroup ref="SubmitCreditMemoActionGroup" stepKey="submitCreditMemo"/> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInCustomer"> + <argument name="Customer" value="$$simpleCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToMyOrdersPage"> + <argument name="menu" value="My Orders"/> + </actionGroup> + <click selector="{{StorefrontCustomerOrderSection.viewOrder}}" stepKey="clickViewOrder"/> + <click selector="{{StorefrontCustomerOrderViewSection.printOrderLink}}" stepKey="clickPrintOrderLink"/> + <waitForPageLoad stepKey="waitPageReload"/> + <switchToWindow stepKey="switchToWindow"/> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBook"> + <argument name="menu" value="Address Book"/> + </actionGroup> + <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="checkShippingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml new file mode 100644 index 0000000000000..b5db354d54371 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml @@ -0,0 +1,75 @@ +<?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="AdminProductBackRedirectNavigateFromCustomerViewCartProduct"> + <annotations> + <features value="Customer"/> + <stories value="Product Back Button"/> + <title value="Product back redirect navigate from customer view cart product"/> + <description value="Back button on product page is redirecting to customer page if opened form shopping cart"/> + <severity value="MINOR"/> + <group value="Customer"/> + </annotations> + <before> + <!-- Create new product--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create new customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Go to storefront as customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Navigate to customer edit page in admin --> + <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$createCustomer.id$$/" stepKey="openCustomerEditPage"/> + <waitForPageLoad stepKey="waitForCustomerEditPage"/> + + <!-- Open shopping cart --> + <click selector="{{AdminCustomerInformationSection.shoppingCart}}" stepKey="clickShoppingCartButton"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + + <!-- Open product --> + <click selector="{{AdminCustomerCartSection.cartItem('1')}}" stepKey="openProduct"/> + + <!-- Go back to customer page --> + <click selector="{{AdminProductFormActionSection.backButton}}" stepKey="goBackToCustomerPage"/> + + <!-- Check current page is customer page --> + <seeInCurrentUrl stepKey="onCustomerAccountPage" url="{{AdminCustomerPage.url}}edit/id/$$createCustomer.id$$/"/> + + <after> + <!--Delete product--> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete category--> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + + <!--Delete customer--> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!-- Sign out--> + <actionGroup ref="SignOut" stepKey="signOut"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 2b24233e8b072..bf8844b2cc7ab 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -25,7 +25,7 @@ <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Step 0: User signs up an account --> <comment userInput="Start of signing up user account" stepKey="startOfSigningUpUserAccount" /> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml index 413bbfd06a539..e2c55eb3962f2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml @@ -24,7 +24,7 @@ </before> <after> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Log in to Storefront as Customer 1 --> @@ -101,7 +101,7 @@ </before> <after> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Log in to Storefront as Customer 1 --> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml index ada3adbfeb83b..40b05153f1a74 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml @@ -20,6 +20,7 @@ </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create Simple Customer --> <createData entity="Simple_US_Customer_CA" stepKey="createSimpleCustomer1"/> @@ -108,6 +109,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Logout --> <actionGroup ref="logout" stepKey="logoutOfAdmin1"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml index 97c932f0cb28a..7d51f97f2463a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml @@ -20,7 +20,7 @@ <group value="create"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml new file mode 100644 index 0000000000000..d36d640c5ad17 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml @@ -0,0 +1,59 @@ +<?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="StorefrontUpdateCustomerAddressBelgiumTest"> + <annotations> + <stories value="Update Regions list for Belgium country"/> + <title value="Update customer address on storefront with Belgium address"/> + <description value="Update customer address on storefront with Belgium address and verify you can select a region"/> + <testCaseId value="MC-20234"/> + <severity value="AVERAGE"/> + <group value="customer"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address Belgium in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + + <!-- Verify country Belgium can be saved without state as state not required --> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickOnDefaultShippingAddress"/> + <selectOption selector="{{StorefrontCustomerAddressFormSection.country}}" userInput="Belgium" stepKey="selectCountry"/> + <selectOption selector="{{StorefrontCustomerAddressFormSection.state}}" userInput="Please select a region, state or province." stepKey="selectState"/> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="Belgium" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> + + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml new file mode 100644 index 0000000000000..285de8d777b48 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml @@ -0,0 +1,51 @@ +<?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="StorefrontUpdateCustomerAddressChinaTest"> + <annotations> + <stories value="Update Regions list for China country"/> + <title value="Update customer address on storefront with china address"/> + <description value="Update customer address on storefront with china address and verify you can select a region"/> + <testCaseId value="MC-20234"/> + <severity value="AVERAGE"/> + <group value="customer"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerChinaAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerChinaAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerChinaAddress"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Account/NavigationTest.php b/app/code/Magento/Customer/Test/Unit/Block/Account/NavigationTest.php new file mode 100644 index 0000000000000..e8c7bd886ab01 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Block/Account/NavigationTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Block\Account; + +use PHPUnit\Framework\TestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Customer\Block\Account\Navigation; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\View\LayoutInterface; +use Magento\Wishlist\Block\Link as WishListLink; +use Magento\Customer\Block\Account\Link as CustomerAccountLink; + +class NavigationTest extends TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Navigation + */ + private $navigation; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var LayoutInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $layoutMock; + + /** + * Setup environment for test + */ + protected function setUp() + { + $this->contextMock = $this->createMock(Context::class); + $this->layoutMock = $this->createMock(LayoutInterface::class); + $this->contextMock->expects($this->any()) + ->method('getLayout') + ->willReturn($this->layoutMock); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->navigation = $this->objectManagerHelper->getObject( + Navigation::class, + [ + 'context' => $this->contextMock + ] + ); + } + + /** + * Test get links with block customer account link and wish list link + * + * @return void + */ + public function testGetLinksWithCustomerAndWishList() + { + $wishListLinkMock = $this->getMockBuilder(WishListLink::class) + ->disableOriginalConstructor() + ->setMethods(['getSortOrder']) + ->getMock(); + + $customerAccountLinkMock = $this->getMockBuilder(CustomerAccountLink::class) + ->disableOriginalConstructor() + ->setMethods(['getSortOrder']) + ->getMock(); + + $wishListLinkMock->expects($this->any()) + ->method('getSortOrder') + ->willReturn(100); + + $customerAccountLinkMock->expects($this->any()) + ->method('getSortOrder') + ->willReturn(20); + + $nameInLayout = 'top.links'; + + $blockChildren = [ + 'wishListLink' => $wishListLinkMock, + 'customerAccountLink' => $customerAccountLinkMock + ]; + + $this->navigation->setNameInLayout($nameInLayout); + $this->layoutMock->expects($this->any()) + ->method('getChildBlocks') + ->with($nameInLayout) + ->willReturn($blockChildren); + + /* Assertion */ + $this->assertEquals( + [ + 0 => $wishListLinkMock, + 1 => $customerAccountLinkMock + ], + $this->navigation->getLinks() + ); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php index 3022177ffb9e1..5ec2ad56560d2 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php @@ -39,7 +39,7 @@ class RegisterTest extends \PHPUnit\Framework\TestCase /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Customer\Model\Session */ private $_customerSession; - /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Module\ModuleManagerInterface */ + /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Module\Manager */ private $_moduleManager; /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Customer\Model\Url */ diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 8bfddac3cef8f..1fd7fc340e542 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -9,13 +9,13 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\ValidationRuleInterface; +use Magento\Customer\Block\Widget\Dob; use Magento\Customer\Helper\Address; use Magento\Framework\App\CacheInterface; use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Data\Form\FilterFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Block\Widget\Dob; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime\Timezone; @@ -23,7 +23,7 @@ use Magento\Framework\View\Element\Html\Date; use Magento\Framework\View\Element\Template\Context; use PHPUnit\Framework\TestCase; -use PHPUnit_Framework_MockObject_MockObject; +use PHPUnit\Framework\MockObject\MockObject; use Zend_Cache_Backend_BlackHole; use Zend_Cache_Core; @@ -60,17 +60,17 @@ class DobTest extends TestCase const YEAR_HTML = '<div><label for="year"><span>yy</span></label><input type="text" id="year" name="Year" value="14"></div>'; - /** @var PHPUnit_Framework_MockObject_MockObject|AttributeMetadataInterface */ + /** @var MockObject|AttributeMetadataInterface */ protected $attribute; /** @var Dob */ protected $_block; - /** @var PHPUnit_Framework_MockObject_MockObject|CustomerMetadataInterface */ + /** @var MockObject|CustomerMetadataInterface */ protected $customerMetadata; /** - * @var FilterFactory|PHPUnit_Framework_MockObject_MockObject + * @var FilterFactory|MockObject */ protected $filterFactory; @@ -336,12 +336,35 @@ public function getYearDataProvider() } /** - * is used to derive the Locale that is used to determine the - * value of Dob::getDateFormat() for that Locale. + * Is used to derive the Locale that is used to determine the value of Dob::getDateFormat() for that Locale + * + * @param string $locale + * @param string $expectedFormat + * @dataProvider getDateFormatDataProvider */ - public function testGetDateFormat() + public function testGetDateFormat(string $locale, string $expectedFormat) { - $this->assertEquals(self::DATE_FORMAT, $this->_block->getDateFormat()); + $this->_locale = $locale; + $this->assertEquals($expectedFormat, $this->_block->getDateFormat()); + } + + /** + * @return array + */ + public function getDateFormatDataProvider(): array + { + return [ + [ + 'ar_SA', + preg_replace( + '/[^MmDdYy\/\.\-]/', + '', + (new \IntlDateFormatter('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE)) + ->getPattern() + ) + ], + [Resolver::DEFAULT_LOCALE, self::DATE_FORMAT], + ]; } /** @@ -521,8 +544,8 @@ public function testGetHtmlExtraParamsWithoutRequiredOption() { $this->escaper->expects($this->any()) ->method('escapeHtml') - ->with('{"validate-date":{"dateFormat":"M\/d\/Y"}}') - ->will($this->returnValue('{"validate-date":{"dateFormat":"M\/d\/Y"}}')); + ->with('{"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}') + ->will($this->returnValue('{"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}')); $this->attribute->expects($this->once()) ->method("isRequired") @@ -530,7 +553,7 @@ public function testGetHtmlExtraParamsWithoutRequiredOption() $this->assertEquals( $this->_block->getHtmlExtraParams(), - 'data-validate="{"validate-date":{"dateFormat":"M\/d\/Y"}}"' + 'data-validate="{"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}"' ); } @@ -544,13 +567,17 @@ public function testGetHtmlExtraParamsWithRequiredOption() ->willReturn(true); $this->escaper->expects($this->any()) ->method('escapeHtml') - ->with('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}') - ->will($this->returnValue('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}')); + ->with('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}') + ->will( + $this->returnValue( + '{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}' + ) + ); $this->context->expects($this->any())->method('getEscaper')->will($this->returnValue($this->escaper)); $this->assertEquals( - 'data-validate="{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}"', + 'data-validate="{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}"', $this->_block->getHtmlExtraParams() ); } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php index 28f897adf9176..5565a807b8135 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php @@ -282,7 +282,7 @@ public function testSuccessMessage($customerId, $key, $vatValidationEnabled, $ad ->willReturnSelf(); $this->messageManagerMock->expects($this->any()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with($this->stringContains($successMessage)) ->willReturnSelf(); @@ -402,7 +402,7 @@ public function testSuccessRedirect( ->willReturnSelf(); $this->messageManagerMock->expects($this->any()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with($this->stringContains($successMessage)) ->willReturnSelf(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php index f8f47eedba3ef..faf55347dba78 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php @@ -346,11 +346,13 @@ public function testSuccessMessage( $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ - ['password', null, $password], - ['password_confirmation', null, $password], - ['is_subscribed', false, true], - ]); + ->willReturnMap( + [ + ['password', null, $password], + ['password_confirmation', null, $password], + ['is_subscribed', false, true], + ] + ); $this->customerMock->expects($this->once()) ->method('setAddresses') @@ -371,7 +373,7 @@ public function testSuccessMessage( ->with($this->equalTo($customerId)); $this->messageManagerMock->expects($this->any()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with($this->stringContains($successMessage)) ->will($this->returnSelf()); @@ -477,11 +479,13 @@ public function testSuccessRedirect( $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturnMap([ - ['password', null, $password], - ['password_confirmation', null, $password], - ['is_subscribed', false, true], - ]); + ->willReturnMap( + [ + ['password', null, $password], + ['password_confirmation', null, $password], + ['is_subscribed', false, true], + ] + ); $this->customerMock->expects($this->once()) ->method('setAddresses') @@ -502,16 +506,18 @@ public function testSuccessRedirect( ->with($this->equalTo($customerId)); $this->messageManagerMock->expects($this->any()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with($this->stringContains($successMessage)) ->will($this->returnSelf()); $this->urlMock->expects($this->any()) ->method('getUrl') - ->willReturnMap([ - ['*/*/index', ['_secure' => true], $successUrl], - ['*/*/create', ['_secure' => true], $successUrl], - ]); + ->willReturnMap( + [ + ['*/*/index', ['_secure' => true], $successUrl], + ['*/*/create', ['_secure' => true], $successUrl], + ] + ); $this->redirectMock->expects($this->once()) ->method('success') ->with($this->equalTo($successUrl)) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php index 762c76b695dee..05a8b6448af99 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php @@ -93,13 +93,14 @@ protected function setUp() $this->session = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() - ->setMethods([ - 'isLoggedIn', - 'setCustomerDataAsLoggedIn', - 'regenerateId', - 'setUsername', - ]) - ->getMock(); + ->setMethods( + [ + 'isLoggedIn', + 'setCustomerDataAsLoggedIn', + 'regenerateId', + 'setUsername', + ] + )->getMock(); $this->accountManagement = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) ->getMockForAbstractClass(); @@ -222,7 +223,7 @@ public function testExecuteEmptyLoginData() ->willReturn([]); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('A login and a password are required.')) ->willReturnSelf(); @@ -253,10 +254,12 @@ public function testExecuteSuccessCustomRedirect() $this->request->expects($this->once()) ->method('getPost') ->with('login') - ->willReturn([ - 'username' => $username, - 'password' => $password, - ]); + ->willReturn( + [ + 'username' => $username, + 'password' => $password, + ] + ); $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMockForAbstractClass(); @@ -292,9 +295,8 @@ public function testExecuteSuccessCustomRedirect() ->method('setCustomerDataAsLoggedIn') ->with($customerMock) ->willReturnSelf(); - $this->session->expects($this->once()) - ->method('regenerateId') - ->willReturnSelf(); + $this->session->expects($this->never()) + ->method('regenerateId'); $this->accountRedirect->expects($this->never()) ->method('getRedirect') @@ -335,10 +337,12 @@ public function testExecuteSuccess() $this->request->expects($this->once()) ->method('getPost') ->with('login') - ->willReturn([ - 'username' => $username, - 'password' => $password, - ]); + ->willReturn( + [ + 'username' => $username, + 'password' => $password, + ] + ); $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMockForAbstractClass(); @@ -357,9 +361,8 @@ public function testExecuteSuccess() ->method('setCustomerDataAsLoggedIn') ->with($customerMock) ->willReturnSelf(); - $this->session->expects($this->once()) - ->method('regenerateId') - ->willReturnSelf(); + $this->session->expects($this->never()) + ->method('regenerateId'); $this->accountRedirect->expects($this->once()) ->method('getRedirect') @@ -426,10 +429,12 @@ public function testExecuteWithException( $this->request->expects($this->once()) ->method('getPost') ->with('login') - ->willReturn([ - 'username' => $username, - 'password' => $password, - ]); + ->willReturn( + [ + 'username' => $username, + 'password' => $password, + ] + ); $exception = new $exceptionData['exception'](__($exceptionData['message'])); @@ -488,11 +493,12 @@ protected function prepareContext() $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() - ->setMethods([ - 'isPost', - 'getPost', - ]) - ->getMock(); + ->setMethods( + [ + 'isPost', + 'getPost', + ] + )->getMock(); $this->resultRedirect = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) ->disableOriginalConstructor() @@ -551,7 +557,7 @@ protected function mockExceptions($exception, $username) $url ); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($message) ->willReturnSelf(); @@ -563,7 +569,7 @@ protected function mockExceptions($exception, $username) case \Magento\Framework\Exception\AuthenticationException::class: $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with( __( 'The account sign-in was incorrect or your account is disabled temporarily. ' @@ -580,7 +586,7 @@ protected function mockExceptions($exception, $username) case '\Exception': $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('An unspecified error occurred. Please contact us for assistance.')) ->willReturnSelf(); break; @@ -591,7 +597,7 @@ protected function mockExceptions($exception, $username) . 'Please wait and try again later.' ); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($message) ->willReturnSelf(); $this->session->expects($this->once()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php index 4064b8586257d..3af3cc60010bb 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php @@ -146,7 +146,7 @@ public function testExecute() ->method('deleteById') ->with($addressId); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You deleted the address.')); $this->resultRedirect->expects($this->once()) ->method('setPath') @@ -183,7 +183,7 @@ public function testExecuteWithException() ->willReturn(34); $exception = new \Exception('Exception'); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t delete the address right now.')) ->willThrowException($exception); $this->messageManager->expects($this->once()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php index 5f7064d5b124b..c9f885315b0ef 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php @@ -167,7 +167,7 @@ public function testExecuteWithTaxClassAndException() ->method('save') ->with($this->group); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the customer group.')); $exception = new \Exception('Exception'); $this->resultRedirect->expects($this->at(0)) @@ -175,7 +175,7 @@ public function testExecuteWithTaxClassAndException() ->with('customer/group') ->willThrowException($exception); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Exception'); $this->dataObjectProcessorMock->expects($this->once()) ->method('buildOutputDataArray') diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php index 45e64f6557d51..8267624f7b006 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Model\EmailNotificationInterface; use Magento\Framework\DataObject; use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Escaper; /** * Unit tests for Inline customer edit @@ -78,6 +79,9 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase /** @var array */ private $items; + /** @var \Magento\Framework\Escaper */ + private $escaper; + /** * Sets up mocks * @@ -86,7 +90,7 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - + $this->escaper = new Escaper(); $this->request = $this->getMockForAbstractClass( \Magento\Framework\App\RequestInterface::class, [], @@ -172,7 +176,8 @@ protected function setUp() 'addressDataFactory' => $this->addressDataFactory, 'addressRepository' => $this->addressRepository, 'logger' => $this->logger, - 'addressRegistry' => $this->addressRegistry + 'addressRegistry' => $this->addressRegistry, + 'escaper' => $this->escaper, ] ); $reflection = new \ReflectionClass(get_class($this->controller)); @@ -291,10 +296,14 @@ protected function prepareMocksForErrorMessagesProcessing() ->willReturn('Error text'); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => ['Error text'], - 'error' => true, - ]) + ->with( + [ + 'messages' => [ + 'Error text', + ], + 'error' => true, + ] + ) ->willReturnSelf(); } @@ -340,10 +349,14 @@ public function testExecuteWithoutItems() $this->resultJson ->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [__('Please correct the data sent.')], - 'error' => true, - ]) + ->with( + [ + 'messages' => [ + __('Please correct the data sent.'), + ], + 'error' => true, + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); } @@ -365,6 +378,7 @@ public function testExecuteLocalizedException() ->method('save') ->with($this->customerData) ->willThrowException($exception); + $this->messageManager->expects($this->once()) ->method('addError') ->with('[Customer ID: 12] Exception message'); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php index cb5ff88ab704a..4157359959ae4 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -199,7 +199,7 @@ public function testExecuteWithException() ->willThrowException(new \Exception('Some message.')); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Some message.'); $this->massAction->execute(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php index 1f39e6306b996..b436b5b137c78 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php @@ -179,7 +179,7 @@ public function testExecuteWithException() ->willThrowException(new \Exception('Some message.')); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Some message.'); $this->massAction->execute(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php index 90bff0b61bcbf..33e578224400b 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php @@ -195,7 +195,7 @@ public function testExecuteWithException() ->willThrowException(new \Exception('Some message.')); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Some message.'); $this->massAction->execute(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php index 1bffa836f5034..971efc0e490bc 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php @@ -195,7 +195,7 @@ public function testExecuteWithException() ->willThrowException(new \Exception('Some message.')); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Some message.'); $this->massAction->execute(); diff --git a/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php b/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php index 03158d05db8e4..15ced1ce66d06 100644 --- a/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php @@ -47,7 +47,7 @@ class CurrentCustomerTest extends \PHPUnit\Framework\TestCase protected $requestMock; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManagerMock; @@ -80,7 +80,7 @@ protected function setUp() $this->customerDataMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); + $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); $this->viewMock = $this->createMock(\Magento\Framework\App\View::class); $this->currentCustomer = new \Magento\Customer\Helper\Session\CurrentCustomer( diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php index 9128d7c675262..bc4c19bc23ac1 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Source/GroupTest.php @@ -6,7 +6,7 @@ namespace Magento\Customer\Test\Unit\Model\Customer\Source; use Magento\Customer\Model\Customer\Source\Group; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchCriteria; @@ -23,7 +23,7 @@ class GroupTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManagerMock; @@ -49,7 +49,7 @@ class GroupTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->moduleManagerMock = $this->getMockBuilder(ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(Manager::class) ->disableOriginalConstructor() ->getMock(); $this->groupRepositoryMock = $this->getMockBuilder(GroupRepositoryInterface::class) diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php index 65831069aa1fb..170cd001e5b9e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php @@ -15,6 +15,7 @@ use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\Math\Random; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -86,6 +87,14 @@ class CustomerTest extends \PHPUnit\Framework\TestCase */ private $dataObjectHelper; + /** + * @var Random|\PHPUnit_Framework_MockObject_MockObject + */ + private $mathRandom; + + /** + * @inheritdoc + */ protected function setUp() { $this->_website = $this->createMock(\Magento\Store\Model\Website::class); @@ -130,6 +139,7 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['populateWithArray']) ->getMock(); + $this->mathRandom = $this->createMock(Random::class); $this->_model = $helper->getObject( \Magento\Customer\Model\Customer::class, @@ -146,7 +156,8 @@ protected function setUp() 'accountConfirmation' => $this->accountConfirmation, '_addressesFactory' => $this->addressesFactory, 'customerDataFactory' => $this->customerDataFactory, - 'dataObjectHelper' => $this->dataObjectHelper + 'dataObjectHelper' => $this->dataObjectHelper, + 'mathRandom' => $this->mathRandom, ] ); } @@ -219,15 +230,17 @@ public function testSendNewAccountEmailWithoutStoreId() ->method('getTransport') ->will($this->returnValue($transportMock)); - $this->_model->setData([ - 'website_id' => 1, - 'store_id' => 1, - 'email' => 'email@example.com', - 'firstname' => 'FirstName', - 'lastname' => 'LastName', - 'middlename' => 'MiddleName', - 'prefix' => 'Name Prefix', - ]); + $this->_model->setData( + [ + 'website_id' => 1, + 'store_id' => 1, + 'email' => 'email@example.com', + 'firstname' => 'FirstName', + 'lastname' => 'LastName', + 'middlename' => 'MiddleName', + 'prefix' => 'Name Prefix', + ] + ); $this->_model->sendNewAccountEmail('registered'); } @@ -383,4 +396,20 @@ public function testGetDataModel() $this->_model->getDataModel(); $this->assertEquals($customerDataObject, $this->_model->getDataModel()); } + + /** + * Check getRandomConfirmationKey use cryptographically secure function + * + * @return void + */ + public function testGetRandomConfirmationKey() : void + { + $this->mathRandom + ->expects($this->once()) + ->method('getRandomString') + ->with(32) + ->willReturn('random_string'); + + $this->_model->getRandomConfirmationKey(); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php index 318023d8068c5..ff83ef62c6aa7 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php @@ -521,7 +521,7 @@ public function testPasswordResetConfirmation() /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ $customer = $this->createMock(CustomerInterface::class); - $customer->expects($this->any()) + $customer->expects($this->once()) ->method('getStoreId') ->willReturn($customerStoreId); $customer->expects($this->any()) @@ -539,11 +539,6 @@ public function testPasswordResetConfirmation() ->method('getStore') ->willReturn($this->storeMock); - $this->storeManagerMock->expects($this->at(1)) - ->method('getStore') - ->with($customerStoreId) - ->willReturn($this->storeMock); - $this->customerRegistryMock->expects($this->once()) ->method('retrieveSecureData') ->with($customerId) diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php index b245702ce07f9..069ddc63d74d7 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php @@ -127,7 +127,7 @@ public function testSaveWithReservedId() ] ) ->getMockForAbstractClass(); - $dbAdapter->expects($this->any())->method('describeTable')->willReturn([]); + $dbAdapter->expects($this->any())->method('describeTable')->willReturn(['customer_group_id' => []]); $dbAdapter->expects($this->any())->method('update')->willReturnSelf(); $dbAdapter->expects($this->once())->method('lastInsertId')->willReturn($expectedId); $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) diff --git a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php index 7efc61af800d3..8565790990df1 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php @@ -66,7 +66,7 @@ protected function setUp() $this->urlFactoryMock = $this->createMock(\Magento\Framework\UrlFactory::class); $this->customerFactoryMock = $this->getMockBuilder(\Magento\Customer\Model\CustomerFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) + ->setMethods(['create', 'save']) ->getMock(); $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -192,15 +192,12 @@ protected function prepareLoginDataMock($customerId) $customerMock = $this->createPartialMock( \Magento\Customer\Model\Customer::class, - ['getId', 'isConfirmationRequired', 'getConfirmation', 'updateData', 'getGroupId'] + ['getId', 'getConfirmation', 'updateData', 'getGroupId'] ); - $customerMock->expects($this->once()) + $customerMock->expects($this->exactly(3)) ->method('getId') ->will($this->returnValue($customerId)); $customerMock->expects($this->once()) - ->method('isConfirmationRequired') - ->will($this->returnValue(true)); - $customerMock->expects($this->never()) ->method('getConfirmation') ->will($this->returnValue($customerId)); diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php index 056c7e71e1827..4a16acd98d827 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Ui\Component\Listing\Column\Actions; +/** + * Class ActionsTest + */ class ActionsTest extends \PHPUnit\Framework\TestCase { /** @var Actions */ @@ -64,7 +67,8 @@ public function testPrepareDataSource() 'edit' => [ 'href' => 'http://magento.com/customer/index/edit', 'label' => new \Magento\Framework\Phrase('Edit'), - 'hidden' => false + 'hidden' => false, + '__disableTmpl' => true, ] ] ], diff --git a/app/code/Magento/Customer/Ui/Component/Form/Field/DisableAutoGroupChange.php b/app/code/Magento/Customer/Ui/Component/Form/Field/DisableAutoGroupChange.php new file mode 100644 index 0000000000000..3404a0e92230c --- /dev/null +++ b/app/code/Magento/Customer/Ui/Component/Form/Field/DisableAutoGroupChange.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Ui\Component\Form\Field; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponentInterface; +use Magento\Customer\Helper\Address as AddressHelper; + +/** + * Process setting to set Default Value for Disable Automatic Group Changes Based on VAT ID + * + * Class \Magento\Customer\Ui\Component\Form\Field\DisableAutoGroupChange + */ +class DisableAutoGroupChange extends \Magento\Ui\Component\Form\Field +{ + /** + * Yes value for Default Value for Disable Automatic Group Changes Based on VAT ID + */ + const DISABLE_AUTO_GROUP_CHANGE_YES = '1'; + + /** + * Address Helper + * + * @var AddressHelper + */ + private $addressHelper; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param AddressHelper $addressHelper + * @param UiComponentInterface[] $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + AddressHelper $addressHelper, + array $components = [], + array $data = [] + ) { + $this->addressHelper = $addressHelper; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * Prepare component configuration + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function prepare() + { + parent::prepare(); + + if ($this->addressHelper->isDisableAutoGroupAssignDefaultValue()) { + $currentConfig = $this->getData('config'); + $currentConfig['default'] = self::DISABLE_AUTO_GROUP_CHANGE_YES; + $this->setData('config', $currentConfig); + } + } +} diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php index d6a4067ef3db6..9441beeb7dc61 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php @@ -60,6 +60,7 @@ public function prepareDataSource(array $dataSource) ), 'label' => __('Edit'), 'hidden' => false, + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php index 6870bd1136d10..5d974088b0d54 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php @@ -79,6 +79,7 @@ public function prepareDataSource(array $dataSource) ] ), 'label' => __('Edit'), + '__disableTmpl' => true ], ]; @@ -96,13 +97,14 @@ public function prepareDataSource(array $dataSource) ), 'label' => __('Delete'), 'confirm' => [ - 'title' => __('Delete %1', $this->escaper->escapeJs($title)), + 'title' => __('Delete %1', $this->escaper->escapeHtml($title)), 'message' => __( 'Are you sure you want to delete a %1 record?', - $this->escaper->escapeJs($title) + $this->escaper->escapeHtml($title) ) ], - 'post' => true + 'post' => true, + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml index c699db06d30dc..e07d7d8708a43 100644 --- a/app/code/Magento/Customer/etc/db_schema.xml +++ b/app/code/Magento/Customer/etc/db_schema.xml @@ -15,7 +15,7 @@ <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Group ID"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" @@ -78,7 +78,7 @@ <table name="customer_address_entity" resource="default" engine="innodb" comment="Customer Address Entity"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Parent ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" @@ -124,9 +124,9 @@ <table name="customer_address_entity_datetime" resource="default" engine="innodb" comment="Customer Address Entity Datetime"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> @@ -155,9 +155,9 @@ <table name="customer_address_entity_decimal" resource="default" engine="innodb" comment="Customer Address Entity Decimal"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" @@ -424,14 +424,14 @@ <column xsi:type="varchar" name="customer_group_code" nullable="false" length="32" comment="Customer Group Code"/> <column xsi:type="int" name="tax_class_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Tax Class Id"/> + default="0" comment="Tax Class ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="customer_group_id"/> </constraint> </table> <table name="customer_eav_attribute" resource="default" engine="innodb" comment="Customer Eav Attribute"> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="smallint" name="is_visible" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Is Visible"/> <column xsi:type="varchar" name="input_filter" nullable="true" length="255" comment="Input Filter"/> @@ -461,7 +461,7 @@ <table name="customer_form_attribute" resource="default" engine="innodb" comment="Customer Form Attribute"> <column xsi:type="varchar" name="form_code" nullable="false" length="32" comment="Form Code"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="form_code"/> <column name="attribute_id"/> @@ -476,9 +476,9 @@ <table name="customer_eav_attribute_website" resource="default" engine="innodb" comment="Customer Eav Attribute Website"> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="is_visible" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Visible"/> <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" @@ -504,7 +504,7 @@ <column xsi:type="bigint" name="visitor_id" padding="20" unsigned="true" nullable="false" identity="true" comment="Visitor ID"/> <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="varchar" name="session_id" nullable="true" length="64" comment="Session ID"/> <column xsi:type="timestamp" name="last_visit_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Last Visit Time"/> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index a181d6dd217fd..be219a81fd990 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -28,11 +28,11 @@ <preference for="Magento\Customer\Api\Data\ValidationResultsInterface" type="Magento\Customer\Model\Data\ValidationResults" /> <preference for="Magento\Customer\Api\Data\GroupSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Customer\Model\GroupSearchResults" /> <preference for="Magento\Customer\Api\Data\CustomerSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Customer\Model\CustomerSearchResults" /> <preference for="Magento\Customer\Api\Data\AddressSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Customer\Model\AddressSearchResults" /> <preference for="Magento\Customer\Api\AccountManagementInterface" type="Magento\Customer\Model\AccountManagement" /> <preference for="Magento\Customer\Api\CustomerMetadataInterface" @@ -157,6 +157,11 @@ <argument name="sectionConfig" xsi:type="object">SectionInvalidationConfigData</argument> </arguments> </type> + <type name="Magento\Customer\Block\SectionNamesProvider"> + <arguments> + <argument name="sectionConfig" xsi:type="object">SectionInvalidationConfigData</argument> + </arguments> + </type> <preference for="Magento\Customer\CustomerData\JsLayoutDataProviderPoolInterface" type="Magento\Customer\CustomerData\JsLayoutDataProviderPool"/> <type name="Magento\Framework\Webapi\ServiceTypeToEntityTypeMap"> @@ -468,4 +473,58 @@ <preference for="Magento\Customer\Api\AccountDelegationInterface" type="Magento\Customer\Model\Delegation\AccountDelegation" /> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="customer" xsi:type="array"> + <item name="confirmation" xsi:type="string">customer</item> + <item name="created_at" xsi:type="string">customer</item> + <item name="created_in" xsi:type="string">customer</item> + <item name="default_billing" xsi:type="string">customer</item> + <item name="default_shipping" xsi:type="string">customer</item> + <item name="disable_auto_group_change" xsi:type="string">customer</item> + <item name="dob" xsi:type="string">customer</item> + <item name="email" xsi:type="string">customer</item> + <item name="failures_num" xsi:type="string">customer</item> + <item name="firstname" xsi:type="string">customer</item> + <item name="first_failure" xsi:type="string">customer</item> + <item name="gender" xsi:type="string">customer</item> + <item name="group_id" xsi:type="string">customer</item> + <item name="lastname" xsi:type="string">customer</item> + <item name="lock_expires" xsi:type="string">customer</item> + <item name="middlename" xsi:type="string">customer</item> + <item name="password_hash" xsi:type="string">customer</item> + <item name="prefix" xsi:type="string">customer</item> + <item name="rp_token" xsi:type="string">customer</item> + <item name="rp_token_created_at" xsi:type="string">customer</item> + <item name="store_id" xsi:type="string">customer</item> + <item name="suffix" xsi:type="string">customer</item> + <item name="taxvat" xsi:type="string">customer</item> + <item name="updated_at" xsi:type="string">customer</item> + <item name="website_id" xsi:type="string">customer</item> + </item> + <item name="customer_address" xsi:type="array"> + <item name="city" xsi:type="string">customer_address</item> + <item name="company" xsi:type="string">customer_address</item> + <item name="country_id" xsi:type="string">customer_address</item> + <item name="fax" xsi:type="string">customer_address</item> + <item name="firstname" xsi:type="string">customer_address</item> + <item name="lastname" xsi:type="string">customer_address</item> + <item name="middlename" xsi:type="string">customer_address</item> + <item name="postcode" xsi:type="string">customer_address</item> + <item name="prefix" xsi:type="string">customer_address</item> + <item name="region" xsi:type="string">customer_address</item> + <item name="region_id" xsi:type="string">customer_address</item> + <item name="street" xsi:type="string">customer_address</item> + <item name="suffix" xsi:type="string">customer_address</item> + <item name="telephone" xsi:type="string">customer_address</item> + <item name="vat_id" xsi:type="string">customer_address</item> + <item name="vat_is_valid" xsi:type="string">customer_address</item> + <item name="vat_request_date" xsi:type="string">customer_address</item> + <item name="vat_request_id" xsi:type="string">customer_address</item> + <item name="vat_request_success" xsi:type="string">customer_address</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index c31742519e581..3b9675178c052 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -77,4 +77,34 @@ </argument> </arguments> </type> -</config> \ No newline at end of file + <type name="Magento\Framework\View\Element\Message\MessageConfigurationsPool"> + <arguments> + <argument name="configurationsMap" xsi:type="array"> + <item name="customerAlreadyExistsErrorMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Customer::messages/customerAlreadyExistsErrorMessage.phtml</item> + </item> + </item> + <item name="confirmAccountSuccessMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Customer::messages/confirmAccountSuccessMessage.phtml</item> + </item> + </item> + <item name="customerVatShippingAddressSuccessMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Customer::messages/customerVatShippingAddressSuccessMessage.phtml</item> + </item> + </item> + <item name="customerVatBillingAddressSuccessMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Customer::messages/customerVatBillingAddressSuccessMessage.phtml</item> + </item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index 3495feb925cb3..a70aa08dba735 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -539,3 +539,4 @@ Addresses,Addresses "Prefix","Prefix" "Middle Name/Initial","Middle Name/Initial" "Suffix","Suffix" +"The Date of Birth should not be greater than today.","The Date of Birth should not be greater than today." diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml index 692cb2ecb964d..3af0172b3fca8 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml @@ -191,13 +191,6 @@ </validation> <dataType>text</dataType> </settings> - <formElements> - <select> - <settings> - <options class="Magento\Directory\Model\ResourceModel\Country\Collection"/> - </settings> - </select> - </formElements> </field> <field name="region_id" component="Magento_Customer/js/form/element/region" formElement="select"> <settings> diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 5fb8b17dbb8c5..7caaeab4f39d6 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -152,8 +152,6 @@ <argument name="data" xsi:type="array"> <item name="type" xsi:type="string">group</item> <item name="config" xsi:type="array"> - <item name="label" xsi:type="string" translate="true">Group</item> - <item name="required" xsi:type="boolean">true</item> <item name="dataScope" xsi:type="boolean">false</item> <item name="validateWholeGroup" xsi:type="boolean">true</item> </item> @@ -166,10 +164,11 @@ </item> </argument> <settings> + <required>true</required> <dataType>number</dataType> </settings> </field> - <field name="disable_auto_group_change" formElement="checkbox"> + <field name="disable_auto_group_change" formElement="checkbox" class="Magento\Customer\Ui\Component\Form\Field\DisableAutoGroupChange"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="fieldGroup" xsi:type="string">group_id</item> @@ -265,10 +264,20 @@ <settings> <validation> <rule name="validate-date" xsi:type="boolean">true</rule> + <rule name="validate-dob" xsi:type="boolean">true</rule> </validation> <dataType>text</dataType> <visible>true</visible> </settings> + <formElements> + <date> + <settings> + <options> + <option name="maxDate" xsi:type="string">-1d</option> + </options> + </settings> + </date> + </formElements> </field> <field name="taxvat" formElement="input"> <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/Customer/view/frontend/email/account_new.html b/app/code/Magento/Customer/view/frontend/email/account_new.html index 6a60aee863eb4..7b77883e41f71 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new.html @@ -4,9 +4,11 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" } @--> @@ -14,7 +16,7 @@ {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Welcome to %store_name." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site, use these credentials during checkout or on the <a href="%customer_url">My Account</a> page:' diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html index 010087ace2d42..364fde0f5150c 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html @@ -4,9 +4,10 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Please confirm your %store_name account" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Please confirm your %store_name account" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/confirm/', [_query:[id:$customer.id, key:$customer.confirmation, back_url:$back_url]])":"Account Confirmation URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,'customer/account/confirm/',[_query:[id:$customer.id,key:$customer.confirmation,back_url:$back_url],_nosid:1])":"Account Confirmation URL", "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html index 931851b28ac21..34e1103fb2f9d 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html @@ -4,16 +4,18 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Thank you for confirming your %store_name account." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Thank you for confirming your %store_name account." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site, use these credentials during checkout or on the <a href="%customer_url">My Account</a> page:' diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html b/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html index 26e417d7da5a7..6d7d89067d8a2 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html @@ -4,16 +4,18 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> -<!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> +<!--@vars { +"var store.frontend_name":"Store Name", +"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Create Password URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Welcome to %store_name." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site and set a password, click on the <a href="%create_password_url">link</a>:' diff --git a/app/code/Magento/Customer/view/frontend/email/change_email.html b/app/code/Magento/Customer/view/frontend/email/change_email.html index f343433fe35e2..4853adf638066 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email.html @@ -4,19 +4,23 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name email has been changed" store_name=$store.getFrontendName()}} @--> -<!--@vars {} @--> +<!--@subject {{trans "Your %store_name email has been changed" store_name=$store.frontend_name}} @--> +<!--@vars { +"var store.frontend_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone" +} @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "Hello,"}}</p> <br> <p> - {{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.getFrontendName()}} + {{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> <br> -<p>{{trans "Thanks,<br>%store_name" store_name=$store.getFrontendName() |raw}}</p> +<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html index 0876e75beacad..49867bdedc9e0 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html @@ -4,19 +4,23 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name email and password has been changed" store_name=$store.getFrontendName()}} @--> -<!--@vars {} @--> +<!--@subject {{trans "Your %store_name email and password has been changed" store_name=$store.frontend_name}} @--> +<!--@vars { +"var store.frontend_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone" +} @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "Hello,"}}</p> <br> <p> - {{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.getFrontendName()}} + {{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> <br> -<p>{{trans "Thanks,<br>%store_name" store_name=$store.getFrontendName() |raw}}</p> +<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_new.html b/app/code/Magento/Customer/view/frontend/email/password_new.html index 1d2468374c6f3..975c8f7254976 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_new.html +++ b/app/code/Magento/Customer/view/frontend/email/password_new.html @@ -4,9 +4,11 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Reset your %store_name password" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Reset your %store_name password" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl(store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL", "var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset.html b/app/code/Magento/Customer/view/frontend/email/password_reset.html index bfa5330cbf5b0..79015117c2280 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset.html @@ -4,9 +4,12 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name password has been changed" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name password has been changed" store_name=$store.frontend_name}} @--> <!--@vars { -"var customer.name":"Customer Name" +"var customer.name":"Customer Name", +"var store.frontend_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone" } @--> {{template config_path="design/email/header_template"}} @@ -14,11 +17,11 @@ <br> <p> - {{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.getFrontendName()}} + {{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> <br> -<p>{{trans "Thanks,<br>%store_name" store_name=$store.getFrontendName() |raw}}</p> +<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html index 59e7f16adfd51..5dc0e2dfafee9 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html @@ -4,10 +4,11 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Reset your %store_name password" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Reset your %store_name password" store_name=$store.frontend_name}} @--> <!--@vars { +"var store.frontend_name":"Store Name", "var customer.name":"Customer Name", -"var this.getUrl($store, 'customer/account/createPassword/', [_query:[id:$customer.id, token:$customer.rp_token]])":"Reset Password URL" +"var this.getUrl($store,'customer/account/createPassword/',[_query:[token:$customer.rp_token],_nosid:1])":"Reset Password URL" } @--> {{template config_path="design/email/header_template"}} diff --git a/app/code/Magento/Customer/view/frontend/layout/default.xml b/app/code/Magento/Customer/view/frontend/layout/default.xml index 94e46fda194b0..3976fc6bd9090 100644 --- a/app/code/Magento/Customer/view/frontend/layout/default.xml +++ b/app/code/Magento/Customer/view/frontend/layout/default.xml @@ -41,9 +41,12 @@ </arguments> </block> <block name="customer.section.config" class="Magento\Customer\Block\SectionConfig" - template="Magento_Customer::js/section-config.phtml"/> - <block name="customer.customer.data" - class="Magento\Customer\Block\CustomerData" + template="Magento_Customer::js/section-config.phtml"> + <arguments> + <argument name="sectionNamesProvider" xsi:type="object">Magento\Customer\Block\SectionNamesProvider</argument> + </arguments> + </block> + <block name="customer.customer.data" class="Magento\Customer\Block\CustomerData" template="Magento_Customer::js/customer-data.phtml"/> <block name="customer.data.invalidation.rules" class="Magento\Customer\Block\CustomerScopeData" template="Magento_Customer::js/customer-data/invalidation-rules.phtml"/> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml index 2718f03909be2..e2b6792439576 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml @@ -67,7 +67,7 @@ </div> </div> </div> - <div class="field confirm password required" data-container="confirm-password"> + <div class="field confirmation password required" data-container="confirm-password"> <label class="label" for="password-confirmation"><span><?= $block->escapeHtml(__('Confirm New Password')) ?></span></label> <div class="control"> <input type="password" class="input-text" name="password_confirmation" id="password-confirmation" @@ -93,7 +93,7 @@ ], function($){ var dataForm = $('#form-validate'); var ignore = <?= /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null' ?>; - + dataForm.mage('validation', { <?php if ($_dob->isEnabled()) : ?> errorPlacement: function(error, element) { diff --git a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml index ebbd16164d7e8..e6511a0674e1d 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml @@ -15,7 +15,9 @@ "baseUrls": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode(array_unique([ $block->getUrl(null, ['_secure' => true]), $block->getUrl(null, ['_secure' => false]), - ])) ?> + ])) ?>, + "sectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class) + ->jsonEncode($block->getData('sectionNamesProvider')->getSectionNames()) ?> } } } diff --git a/app/code/Magento/Customer/view/frontend/templates/messages/confirmAccountSuccessMessage.phtml b/app/code/Magento/Customer/view/frontend/templates/messages/confirmAccountSuccessMessage.phtml new file mode 100644 index 0000000000000..a2edb20e967c6 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/messages/confirmAccountSuccessMessage.phtml @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +?> +<?= $block->escapeHtml(__('You must confirm your account. Please check your email for the confirmation link or <a href="%1">click here</a> for a new link.', $block->getData('url')), ['a']); diff --git a/app/code/Magento/Customer/view/frontend/templates/messages/customerAlreadyExistsErrorMessage.phtml b/app/code/Magento/Customer/view/frontend/templates/messages/customerAlreadyExistsErrorMessage.phtml new file mode 100644 index 0000000000000..32982551b5b16 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/messages/customerAlreadyExistsErrorMessage.phtml @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +?> +<?= $block->escapeHtml(__('There is already an account with this email address. If you are sure that it is your email address, <a href="%1">click here</a> to get your password and access your account.', $block->getData('url')), ['a']); diff --git a/app/code/Magento/Customer/view/frontend/templates/messages/customerVatBillingAddressSuccessMessage.phtml b/app/code/Magento/Customer/view/frontend/templates/messages/customerVatBillingAddressSuccessMessage.phtml new file mode 100644 index 0000000000000..294412cbc706d --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/messages/customerVatBillingAddressSuccessMessage.phtml @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +?> +<?= $block->escapeHtml(__('If you are a registered VAT customer, please <a href="%1">click here</a> to enter your billing address for proper VAT calculation.', $block->getData('url')), ['a']); diff --git a/app/code/Magento/Customer/view/frontend/templates/messages/customerVatShippingAddressSuccessMessage.phtml b/app/code/Magento/Customer/view/frontend/templates/messages/customerVatShippingAddressSuccessMessage.phtml new file mode 100644 index 0000000000000..72dbf3f5e03d4 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/messages/customerVatShippingAddressSuccessMessage.phtml @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +?> +<?= $block->escapeHtml(__('If you are a registered VAT customer, please <a href="%1">click here</a> to enter your shipping address for proper VAT calculation.', $block->getData('url')), ['a']); diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index ac4b9f93e0c54..3c2f970faadee 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -35,3 +35,11 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; <?php endif; ?> </div> </div> + +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/validation": {} + } + } + </script> diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml index c444a15705909..4c3432233189b 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml @@ -14,18 +14,14 @@ </span> </label> <div class="control"> - <?php - $_validationClass = $block->escapeHtmlAttr( - $this->helper(\Magento\Customer\Helper\Address::class) - ->getAttributeValidationClass('telephone') - ); - ?> <input type="text" name="telephone" id="telephone" value="<?= $block->escapeHtmlAttr($block->getTelephone()) ?>" title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" - class="input-text <?= $_validationClass ?: '' ?>" + class="input-text <?= $block->escapeHtmlAttr( + $block->getAttributeValidationClass('telephone') + ) ?>" > </div> </div> diff --git a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js index 41cf05df2b1d5..de3ff10bb057b 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js +++ b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js @@ -198,30 +198,9 @@ define([ * Customer data initialization */ init: function () { - var privateContentVersion = 'private_content_version', - privateContent = $.cookieStorage.get(privateContentVersion), - localPrivateContent = $.localStorage.get(privateContentVersion), - needVersion = 'need_version', - expiredSectionNames = this.getExpiredSectionNames(); - - if (privateContent && - !$.cookieStorage.isSet(privateContentVersion) && - !$.localStorage.isSet(privateContentVersion) - ) { - $.cookieStorage.set(privateContentVersion, needVersion); - $.localStorage.set(privateContentVersion, needVersion); - this.reload([], false); - } else if (localPrivateContent !== privateContent) { - if (!$.cookieStorage.isSet(privateContentVersion)) { - privateContent = needVersion; - $.cookieStorage.set(privateContentVersion, privateContent); - } - $.localStorage.set(privateContentVersion, privateContent); - _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) { - buffer.notify(sectionName, sectionData); - }); - this.reload([], false); - } else if (expiredSectionNames.length > 0) { + var expiredSectionNames = this.getExpiredSectionNames(); + + if (expiredSectionNames.length > 0) { _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) { buffer.notify(sectionName, sectionData); }); @@ -341,7 +320,9 @@ define([ var sectionDataIds, sectionsNamesForInvalidation; - sectionsNamesForInvalidation = _.contains(sectionNames, '*') ? buffer.keys() : sectionNames; + sectionsNamesForInvalidation = _.contains(sectionNames, '*') ? sectionConfig.getSectionNames() : + sectionNames; + $(document).trigger('customer-data-invalidate', [sectionsNamesForInvalidation]); buffer.remove(sectionsNamesForInvalidation); sectionDataIds = $.cookieStorage.get('section_data_ids') || {}; diff --git a/app/code/Magento/Customer/view/frontend/web/js/section-config.js b/app/code/Magento/Customer/view/frontend/web/js/section-config.js index 76fe7f2515a3a..d346d5b070729 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/section-config.js +++ b/app/code/Magento/Customer/view/frontend/web/js/section-config.js @@ -6,7 +6,7 @@ define(['underscore'], function (_) { 'use strict'; - var baseUrls, sections, clientSideSections, canonize; + var baseUrls, sections, clientSideSections, sectionNames, canonize; /** * @param {String} url @@ -70,6 +70,15 @@ define(['underscore'], function (_) { return _.contains(clientSideSections, sectionName); }, + /** + * Returns array of section names. + * + * @returns {Array} + */ + getSectionNames: function () { + return sectionNames; + }, + /** * @param {Object} options * @constructor @@ -78,6 +87,7 @@ define(['underscore'], function (_) { baseUrls = options.baseUrls; sections = options.sections; clientSideSections = options.clientSideSections; + sectionNames = options.sectionNames; } }; }); diff --git a/app/code/Magento/Customer/view/frontend/web/js/validation.js b/app/code/Magento/Customer/view/frontend/web/js/validation.js new file mode 100644 index 0000000000000..67a714212026a --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/validation.js @@ -0,0 +1,20 @@ +define([ + 'jquery', + 'moment', + 'jquery/validate', + 'mage/translate' +], function ($, moment) { + 'use strict'; + + $.validator.addMethod( + 'validate-dob', + function (value) { + if (value === '') { + return true; + } + + return moment(value).isBefore(moment()); + }, + $.mage.__('The Date of Birth should not be greater than today.') + ); +}); diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php index 388b6dc2ea943..37230df202a6f 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php @@ -10,9 +10,10 @@ use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Directory\Helper\Data as DirectoryData; +use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\Api\DataObjectHelper; /** * Create customer address @@ -38,23 +39,30 @@ class CreateCustomerAddress * @var DataObjectHelper */ private $dataObjectHelper; + /** + * @var DirectoryData + */ + private $directoryData; /** * @param GetAllowedAddressAttributes $getAllowedAddressAttributes * @param AddressInterfaceFactory $addressFactory * @param AddressRepositoryInterface $addressRepository * @param DataObjectHelper $dataObjectHelper + * @param DirectoryData $directoryData */ public function __construct( GetAllowedAddressAttributes $getAllowedAddressAttributes, AddressInterfaceFactory $addressFactory, AddressRepositoryInterface $addressRepository, - DataObjectHelper $dataObjectHelper + DataObjectHelper $dataObjectHelper, + DirectoryData $directoryData ) { $this->getAllowedAddressAttributes = $getAllowedAddressAttributes; $this->addressFactory = $addressFactory; $this->addressRepository = $addressRepository; $this->dataObjectHelper = $dataObjectHelper; + $this->directoryData = $directoryData; } /** @@ -67,6 +75,10 @@ public function __construct( */ public function execute(int $customerId, array $data): AddressInterface { + // It is needed because AddressInterface has country_id field. + if (isset($data['country_code'])) { + $data['country_id'] = $data['country_code']; + } $this->validateData($data); /** @var AddressInterface $address */ @@ -98,6 +110,13 @@ public function validateData(array $addressData): void $attributes = $this->getAllowedAddressAttributes->execute(); $errorInput = []; + //Add error for empty postcode with country with no optional ZIP + if (!$this->directoryData->isZipCodeOptional($addressData['country_id']) + && (!isset($addressData['postcode']) || empty($addressData['postcode'])) + ) { + $errorInput[] = 'postcode'; + } + foreach ($attributes as $attributeName => $attributeInfo) { if ($attributeInfo->getIsRequired() && (!isset($addressData[$attributeName]) || empty($addressData[$attributeName])) diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php index a4649bccc02e8..5a302f4c3df27 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php @@ -105,6 +105,7 @@ public function execute(AddressInterface $address): array foreach ($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) { $isArray = false; if (is_array($attribute['value'])) { + // @ignoreCoverageStart $isArray = true; foreach ($attribute['value'] as $attributeValue) { if (is_array($attributeValue)) { @@ -116,6 +117,7 @@ public function execute(AddressInterface $address): array $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); continue; } + // @ignoreCoverageEnd } if ($isArray) { continue; @@ -125,6 +127,12 @@ public function execute(AddressInterface $address): array } $addressData = array_merge($addressData, $customAttributes); + $addressData['customer_id'] = null; + + if (isset($addressData['country_id'])) { + $addressData['country_code'] = $addressData['country_id']; + } + return $addressData; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php index 65745a20bc8eb..26e53c7c3a0a8 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php @@ -66,6 +66,9 @@ public function __construct( */ public function execute(AddressInterface $address, array $data): void { + if (isset($data['country_code'])) { + $data['country_id'] = $data['country_code']; + } $this->validateData($data); $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php index 3cc831e1ca40e..c252628b6566e 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php @@ -9,12 +9,8 @@ use Magento\Customer\Model\AuthenticationInterface; use Magento\Framework\Exception\InvalidEmailOrPasswordException; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** * Check customer password @@ -41,8 +37,6 @@ public function __construct( * @param string $password * @param int $customerId * @throws GraphQlAuthenticationException - * @throws GraphQlInputException - * @throws GraphQlNoSuchEntityException */ public function execute(string $password, int $customerId) { @@ -52,10 +46,6 @@ public function execute(string $password, int $customerId) throw new GraphQlAuthenticationException(__($e->getMessage()), $e); } catch (UserLockedException $e) { throw new GraphQlAuthenticationException(__($e->getMessage()), $e); - } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); - } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php index de37482aca056..c62a931809644 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php @@ -101,8 +101,16 @@ public function execute(CustomerInterface $customer): array } } $customerData = array_merge($customerData, $customAttributes); + //Fields are deprecated and should not be exposed on storefront. + $customerData['group_id'] = null; + $customerData['id'] = null; $customerData['model'] = $customer; + + //'dob' is deprecated, 'date_of_birth' is used instead. + if (!empty($customerData['dob'])) { + $customerData['date_of_birth'] = $customerData['dob']; + } return $customerData; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php index 63f42ea1825af..812b3fd283525 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php @@ -70,6 +70,7 @@ public function execute(ContextInterface $context): CustomerInterface try { $customer = $this->customerRepository->getById($currentUserId); + // @codeCoverageIgnoreStart } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException( __('Customer with id "%customer_id" does not exist.', ['customer_id' => $currentUserId]), @@ -77,6 +78,7 @@ public function execute(ContextInterface $context): CustomerInterface ); } catch (LocalizedException $e) { throw new GraphQlInputException(__($e->getMessage())); + // @codeCoverageIgnoreEnd } if (true === $this->authentication->isLocked($currentUserId)) { @@ -85,8 +87,10 @@ public function execute(ContextInterface $context): CustomerInterface try { $confirmationStatus = $this->accountManagement->getConfirmationStatus($currentUserId); + // @codeCoverageIgnoreStart } catch (LocalizedException $e) { throw new GraphQlInputException(__($e->getMessage())); + // @codeCoverageIgnoreEnd } if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php index 1f730f2a5c7e6..c690e11bd4940 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php @@ -13,6 +13,8 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Newsletter\Model\Config; +use Magento\Store\Model\ScopeInterface; /** * Create customer account resolver @@ -30,13 +32,23 @@ class CreateCustomer implements ResolverInterface private $createCustomerAccount; /** + * @var Config + */ + private $newsLetterConfig; + + /** + * CreateCustomer constructor. + * * @param ExtractCustomerData $extractCustomerData * @param CreateCustomerAccount $createCustomerAccount + * @param Config $newsLetterConfig */ public function __construct( ExtractCustomerData $extractCustomerData, - CreateCustomerAccount $createCustomerAccount + CreateCustomerAccount $createCustomerAccount, + Config $newsLetterConfig ) { + $this->newsLetterConfig = $newsLetterConfig; $this->extractCustomerData = $extractCustomerData; $this->createCustomerAccount = $createCustomerAccount; } @@ -55,6 +67,12 @@ public function resolve( throw new GraphQlInputException(__('"input" value should be specified')); } + if (!$this->newsLetterConfig->isActive(ScopeInterface::SCOPE_STORE)) { + $args['input']['is_subscribed'] = false; + } + if (isset($args['input']['date_of_birth'])) { + $args['input']['dob'] = $args['input']['date_of_birth']; + } $customer = $this->createCustomerAccount->execute( $args['input'], $context->getExtensionAttributes()->getStore() diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php index da588b98b5dbc..c26b3710ef561 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php @@ -58,10 +58,6 @@ public function resolve( throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } - if (empty($args['id'])) { - throw new GraphQlInputException(__('Address "id" value should be specified')); - } - $address = $this->getCustomerAddress->execute((int)$args['id'], $context->getUserId()); $this->deleteCustomerAddress->execute($address); return true; diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php index b2ef03fc40e5a..f2b0d0e2a0495 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php @@ -70,6 +70,9 @@ public function resolve( if (empty($args['input']) || !is_array($args['input'])) { throw new GraphQlInputException(__('"input" value should be specified')); } + if (isset($args['input']['date_of_birth'])) { + $args['input']['dob'] = $args['input']['date_of_birth']; + } $customer = $this->getCustomer->execute($context); $this->updateCustomerAccount->execute( diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 911624da8fe57..70a98f728b696 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -4,7 +4,6 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0||~7.3.0", - "magento/module-customer": "*", "magento/module-authorization": "*", "magento/module-customer": "*", "magento/module-eav": "*", @@ -12,7 +11,8 @@ "magento/module-newsletter": "*", "magento/module-integration": "*", "magento/module-store": "*", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-directory": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index d27debdc39c64..9aa1fdaa841e4 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -28,7 +28,8 @@ input CustomerAddressInput { city: String @doc(description: "The city or town") region: CustomerAddressRegionInput @doc(description: "An object containing the region name, region code, and region ID") postcode: String @doc(description: "The customer's ZIP or postal code") - country_id: CountryCodeEnum @doc(description: "The customer's country") + country_id: CountryCodeEnum @doc(description: "Deprecated: use `country_code` instead.") + country_code: CountryCodeEnum @doc(description: "The customer's country") default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") fax: String @doc(description: "The fax number") @@ -36,13 +37,13 @@ input CustomerAddressInput { prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III") vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") - custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Address custom attributes") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated: Custom attributes should not be put into container.") } input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") { region_code: String @doc(description: "The address region code") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "Uniquely identifies the region") + region_id: Int @doc(description: "region_id is deprecated. Region ID is excessive on storefront and region code should suffice for all scenarios") } input CustomerAddressAttributeInput { @@ -60,10 +61,11 @@ input CustomerInput { middlename: String @doc(description: "The customer's middle name") lastname: String @doc(description: "The customer's family name") suffix: String @doc(description: "A value such as Sr., Jr., or III") - email: String @doc(description: "The customer's email address. Required") - dob: String @doc(description: "The customer's date of birth") + email: String @doc(description: "The customer's email address. Required for customer creation") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") - gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") password: String @doc(description: "The customer's password") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") } @@ -78,7 +80,7 @@ type RevokeCustomerTokenOutput { type Customer @doc(description: "Customer defines the customer name and address and other details") { created_at: String @doc(description: "Timestamp indicating when the account was created") - group_id: Int @doc(description: "The group assigned to the user. Default values are 0 (Not logged in), 1 (General), 2 (Wholesale), and 3 (Retailer)") + group_id: Int @deprecated(reason: "Customer group should not be exposed in the storefront scenarios") prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") firstname: String @doc(description: "The customer's first name") middlename: String @doc(description: "The customer's middle name") @@ -87,20 +89,22 @@ type Customer @doc(description: "Customer defines the customer name and address email: String @doc(description: "The customer's email address. Required") default_billing: String @doc(description: "The ID assigned to the billing address") default_shipping: String @doc(description: "The ID assigned to the shipping address") - dob: String @doc(description: "The customer's date of birth") - taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") - id: Int @doc(description: "The ID assigned to the customer") + dob: String @doc(description: "The customer's date of birth") @deprecated(reason: "Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") + id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") - gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") } type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ id: Int @doc(description: "The ID assigned to the address object") - customer_id: Int @doc(description: "The customer ID") + customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") - region_id: Int @doc(description: "A number that uniquely identifies the state, province, or other area") - country_id: String @doc(description: "The customer's country") + region_id: Int @deprecated(reason: "Region ID is excessive on storefront and region code should suffice for all scenarios") + country_id: String @doc(description: "The customer's country") @deprecated(reason: "Use `country_code` instead.") + country_code: CountryCodeEnum @doc(description: "The customer's country") street: [String] @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's company") telephone: String @doc(description: "The telephone number") @@ -112,17 +116,17 @@ type CustomerAddress @doc(description: "CustomerAddress contains detailed inform middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III") - vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") - custom_attributes: [CustomerAddressAttribute] @doc(description: "Address custom attributes") + custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into container") extension_attributes: [CustomerAddressAttribute] @doc(description: "Address extension attributes") } type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") { region_code: String @doc(description: "The address region code") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "Uniquely identifies the region") + region_id: Int @deprecated(reason: "Region ID is excessive on storefront and region code should suffice for all scenarios") } type CustomerAddressAttribute { diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 14759bd130f2b..f86ebaea69730 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CustomerImportExport\Model\Import; use Magento\Customer\Api\Data\CustomerInterface; @@ -21,7 +23,7 @@ class Customer extends AbstractCustomer { /** - * Attribute collection name + * Collection name attribute */ const ATTRIBUTE_COLLECTION_NAME = \Magento\Customer\Model\ResourceModel\Attribute\Collection::class; @@ -519,8 +521,10 @@ protected function _importData() ); } elseif ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE) { $processedData = $this->_prepareDataForUpdate($rowData); + // phpcs:disable Magento2.Performance.ForeachArrayMerge $entitiesToCreate = array_merge($entitiesToCreate, $processedData[self::ENTITIES_TO_CREATE_KEY]); $entitiesToUpdate = array_merge($entitiesToUpdate, $processedData[self::ENTITIES_TO_UPDATE_KEY]); + // phpcs:enable foreach ($processedData[self::ATTRIBUTES_TO_SAVE_KEY] as $tableName => $customerAttributes) { if (!isset($attributesToSave[$tableName])) { $attributesToSave[$tableName] = []; @@ -598,14 +602,18 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $isFieldNotSetAndCustomerDoesNotExist = !isset($rowData[$attributeCode]) && !$this->_getCustomerId($email, $website); $isFieldSetAndTrimmedValueIsEmpty - = isset($rowData[$attributeCode]) && '' === trim($rowData[$attributeCode]); + = isset($rowData[$attributeCode]) && '' === trim((string)$rowData[$attributeCode]); if ($isFieldRequired && ($isFieldNotSetAndCustomerDoesNotExist || $isFieldSetAndTrimmedValueIsEmpty)) { $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); continue; } - if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { + if (isset($rowData[$attributeCode]) && strlen((string)$rowData[$attributeCode])) { + if ($attributeParams['type'] == 'select') { + continue; + } + $this->isAttributeValid( $attributeCode, $attributeParams, diff --git a/app/code/Magento/Deploy/Collector/Collector.php b/app/code/Magento/Deploy/Collector/Collector.php index 7742f2971a2fe..b09001a7ac04c 100644 --- a/app/code/Magento/Deploy/Collector/Collector.php +++ b/app/code/Magento/Deploy/Collector/Collector.php @@ -9,7 +9,7 @@ use Magento\Deploy\Package\Package; use Magento\Deploy\Package\PackageFactory; use Magento\Deploy\Package\PackageFile; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\View\Asset\PreProcessor\FileNameResolver; /** @@ -45,8 +45,8 @@ class Collector implements CollectorInterface * @var PackageFactory */ private $packageFactory; - - /** @var \Magento\Framework\Module\ModuleManagerInterface */ + + /** @var \Magento\Framework\Module\Manager */ private $moduleManager; /** @@ -66,19 +66,19 @@ class Collector implements CollectorInterface * @param SourcePool $sourcePool * @param FileNameResolver $fileNameResolver * @param PackageFactory $packageFactory - * @param ModuleManagerInterface|null $moduleManager + * @param Manager|null $moduleManager */ public function __construct( SourcePool $sourcePool, FileNameResolver $fileNameResolver, PackageFactory $packageFactory, - ModuleManagerInterface $moduleManager = null + Manager $moduleManager = null ) { $this->sourcePool = $sourcePool; $this->fileNameResolver = $fileNameResolver; $this->packageFactory = $packageFactory; $this->moduleManager = $moduleManager ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Module\ModuleManagerInterface::class); + ->get(\Magento\Framework\Module\Manager::class); } /** diff --git a/app/code/Magento/Deploy/Console/DeployStaticOptions.php b/app/code/Magento/Deploy/Console/DeployStaticOptions.php index 1c02d24f7e99c..06887fc0206fc 100644 --- a/app/code/Magento/Deploy/Console/DeployStaticOptions.php +++ b/app/code/Magento/Deploy/Console/DeployStaticOptions.php @@ -78,6 +78,11 @@ class DeployStaticOptions */ const NO_JAVASCRIPT = 'no-javascript'; + /** + * Key for js-bundle option + */ + const NO_JS_BUNDLE = 'no-js-bundle'; + /** * Key for css option */ @@ -122,9 +127,6 @@ class DeployStaticOptions */ const NO_LESS = 'no-less'; - /** - * Default jobs amount - */ const DEFAULT_JOBS_AMOUNT = 0; /** @@ -275,6 +277,12 @@ private function getSkipOptions() InputOption::VALUE_NONE, 'Do not deploy JavaScript files.' ), + new InputOption( + self::NO_JS_BUNDLE, + null, + InputOption::VALUE_NONE, + 'Do not deploy JavaScript bundle files.' + ), new InputOption( self::NO_CSS, null, diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index 2e924d41a1b83..423f3072c4620 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -459,17 +459,17 @@ public function getParentMap() */ public function getParentFiles($type = null) { - $files = []; + $files = [[]]; foreach ($this->getParentPackages() as $parentPackage) { if ($type === null) { // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge - $files = array_merge($files, $parentPackage->getFiles()); + $files[] = $parentPackage->getFiles(); } else { // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge - $files = array_merge($files, $parentPackage->getFilesByType($type)); + $files[] = $parentPackage->getFilesByType($type); } } - return $files; + return array_merge(...$files); } /** diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php index 2f2a149239990..6c8db345187cc 100644 --- a/app/code/Magento/Deploy/Process/Queue.php +++ b/app/code/Magento/Deploy/Process/Queue.php @@ -161,6 +161,7 @@ public function getPackages() public function process() { $returnStatus = 0; + $logDelay = 10; $this->start = $this->lastJobStarted = time(); $packages = $this->packages; while (count($packages) && $this->checkTimeout()) { @@ -168,12 +169,24 @@ public function process() // Unsets each member of $packages array (passed by reference) as each is executed $this->assertAndExecute($name, $packages, $packageJob); } - $this->logger->info('.'); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - sleep(3); - foreach ($this->inProgress as $name => $package) { - if ($this->isDeployed($package)) { - unset($this->inProgress[$name]); + + // refresh current status in console once in 10 iterations (once in 5 sec) + if ($logDelay >= 10) { + $this->logger->info('.'); + $logDelay = 0; + } else { + $logDelay++; + } + + if ($this->isCanBeParalleled()) { + // in parallel mode sleep before trying to check status and run new jobs + // phpcs:ignore Magento2.Functions.DiscouragedFunction + usleep(500000); // 0.5 sec (less sleep == less time waste) + + foreach ($this->inProgress as $name => $package) { + if ($this->isDeployed($package)) { + unset($this->inProgress[$name]); + } } } } @@ -243,15 +256,25 @@ private function executePackage(Package $package, string $name, array &$packages */ private function awaitForAllProcesses() { + $logDelay = 10; while ($this->inProgress && $this->checkTimeout()) { foreach ($this->inProgress as $name => $package) { if ($this->isDeployed($package)) { unset($this->inProgress[$name]); } } - $this->logger->info('.'); + + // refresh current status in console once in 10 iterations (once in 5 sec) + if ($logDelay >= 10) { + $this->logger->info('.'); + $logDelay = 0; + } else { + $logDelay++; + } + + // sleep before checking parallel jobs status // phpcs:ignore Magento2.Functions.DiscouragedFunction - sleep(5); + usleep(500000); // 0.5 sec (less sleep == less time waste) } if ($this->isCanBeParalleled()) { // close connections only if ran with forks diff --git a/app/code/Magento/Deploy/Service/Bundle.php b/app/code/Magento/Deploy/Service/Bundle.php index f16b93a185595..26e61624c219e 100644 --- a/app/code/Magento/Deploy/Service/Bundle.php +++ b/app/code/Magento/Deploy/Service/Bundle.php @@ -216,7 +216,7 @@ private function isExcluded($filePath, $area, $theme) $excludedFiles = $this->bundleConfig->getExcludedFiles($area, $theme); foreach ($excludedFiles as $excludedFileId) { $excludedFilePath = $this->prepareExcludePath($excludedFileId); - if ($excludedFilePath === $filePath) { + if ($excludedFilePath === $filePath || $excludedFilePath === str_replace('.min.js', '.js', $filePath)) { return true; } } diff --git a/app/code/Magento/Deploy/Service/DeployPackage.php b/app/code/Magento/Deploy/Service/DeployPackage.php index 34a6b147a0551..90d4cdb116969 100644 --- a/app/code/Magento/Deploy/Service/DeployPackage.php +++ b/app/code/Magento/Deploy/Service/DeployPackage.php @@ -249,23 +249,6 @@ private function checkFileSkip($filePath, array $options) */ private function register(Package $package, PackageFile $file = null, $skipLogging = false) { - $logMessage = '.'; - if ($file) { - $logMessage = "Processing file '{$file->getSourcePath()}'"; - if ($file->getArea()) { - $logMessage .= " for area '{$file->getArea()}'"; - } - if ($file->getTheme()) { - $logMessage .= ", theme '{$file->getTheme()}'"; - } - if ($file->getLocale()) { - $logMessage .= ", locale '{$file->getLocale()}'"; - } - if ($file->getModule()) { - $logMessage .= "module '{$file->getModule()}'"; - } - } - $info = [ 'count' => $this->count, 'last' => $file ? $file->getSourcePath() : '' @@ -273,6 +256,23 @@ private function register(Package $package, PackageFile $file = null, $skipLoggi $this->deployStaticFile->writeTmpFile('info.json', $package->getPath(), json_encode($info)); if (!$skipLogging) { + $logMessage = '.'; + if ($file) { + $logMessage = "Processing file '{$file->getSourcePath()}'"; + if ($file->getArea()) { + $logMessage .= " for area '{$file->getArea()}'"; + } + if ($file->getTheme()) { + $logMessage .= ", theme '{$file->getTheme()}'"; + } + if ($file->getLocale()) { + $logMessage .= ", locale '{$file->getLocale()}'"; + } + if ($file->getModule()) { + $logMessage .= "module '{$file->getModule()}'"; + } + } + $this->logger->info($logMessage); } } diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php index 8903997159914..b6333d6fec71e 100644 --- a/app/code/Magento/Deploy/Service/DeployStaticContent.php +++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php @@ -5,9 +5,9 @@ */ namespace Magento\Deploy\Service; -use Magento\Deploy\Strategy\DeployStrategyFactory; -use Magento\Deploy\Process\QueueFactory; use Magento\Deploy\Console\DeployStaticOptions as Options; +use Magento\Deploy\Process\QueueFactory; +use Magento\Deploy\Strategy\DeployStrategyFactory; use Magento\Framework\App\View\Deployment\Version\StorageInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; @@ -75,6 +75,9 @@ public function __construct( * @param array $options * @throws LocalizedException * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function deploy(array $options) { @@ -106,27 +109,35 @@ public function deploy(array $options) $deployStrategy = $this->deployStrategyFactory->create( $options[Options::STRATEGY], - [ - 'queue' => $this->queueFactory->create($queueOptions) - ] + ['queue' => $this->queueFactory->create($queueOptions)] ); $packages = $deployStrategy->deploy($options); if ($options[Options::NO_JAVASCRIPT] !== true) { - $deployRjsConfig = $this->objectManager->create(DeployRequireJsConfig::class, [ - 'logger' => $this->logger - ]); - $deployI18n = $this->objectManager->create(DeployTranslationsDictionary::class, [ - 'logger' => $this->logger - ]); - $deployBundle = $this->objectManager->create(Bundle::class, [ - 'logger' => $this->logger - ]); + $deployRjsConfig = $this->objectManager->create( + DeployRequireJsConfig::class, + ['logger' => $this->logger] + ); + $deployI18n = $this->objectManager->create( + DeployTranslationsDictionary::class, + ['logger' => $this->logger] + ); foreach ($packages as $package) { if (!$package->isVirtual()) { $deployRjsConfig->deploy($package->getArea(), $package->getTheme(), $package->getLocale()); $deployI18n->deploy($package->getArea(), $package->getTheme(), $package->getLocale()); + } + } + } + + if ($options[Options::NO_JAVASCRIPT] !== true && $options[Options::NO_JS_BUNDLE] !== true) { + $deployBundle = $this->objectManager->create( + Bundle::class, + ['logger' => $this->logger] + ); + foreach ($packages as $package) { + if (!$package->isVirtual()) { $deployBundle->deploy($package->getArea(), $package->getTheme(), $package->getLocale()); } } diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml new file mode 100644 index 0000000000000..3a7d3663c8875 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MagentoDeveloperModeOnlyTestSuite"> + <before> + <magentoCLI command="deploy:mode:set developer" stepKey="enableDeveloperMode"/> + </before> + <include> + <group name="developer_mode_only"/> + </include> + <after> + <!-- Command should be uncommented once MQE-1711 is resolved --> + <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> + <!-- <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> --> + </after> + </suite> +</suites> diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml new file mode 100644 index 0000000000000..bf7014cdbb49d --- /dev/null +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MagentoProductionModeOnlyTestSuite"> + <before> + <!-- Command should be uncommented once MQE-1711 is resolved --> + <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> + <!-- <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> --> + </before> + <include> + <group name="production_mode_only"/> + </include> + <after> + <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> + </after> + </suite> +</suites> diff --git a/app/code/Magento/Deploy/Test/Unit/Process/QueueTest.php b/app/code/Magento/Deploy/Test/Unit/Process/QueueTest.php index dc32de527c8c1..540826c67b790 100644 --- a/app/code/Magento/Deploy/Test/Unit/Process/QueueTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Process/QueueTest.php @@ -112,6 +112,7 @@ public function testProcess() $package->expects($this->any())->method('getArea')->willReturn('area'); $package->expects($this->any())->method('getPath')->willReturn('path'); $package->expects($this->any())->method('getFiles')->willReturn([]); + $this->logger->expects($this->exactly(2))->method('info')->willReturnSelf(); $this->appState->expects($this->once())->method('emulateAreaCode'); diff --git a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php index 396381960e544..fcc02476bb858 100644 --- a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php @@ -103,7 +103,7 @@ public function testDeploy($options, $expectedContentVersion) $package->expects($this->never())->method('getTheme'); $package->expects($this->never())->method('getLocale'); } else { - $package->expects($this->exactly(1))->method('isVirtual')->willReturn(false); + $package->expects($this->exactly(2))->method('isVirtual')->willReturn(false); $package->expects($this->exactly(3))->method('getArea')->willReturn('area'); $package->expects($this->exactly(3))->method('getTheme')->willReturn('theme'); $package->expects($this->exactly(3))->method('getLocale')->willReturn('locale'); @@ -198,6 +198,7 @@ public function deployDataProvider() [ 'strategy' => 'compact', 'no-javascript' => false, + 'no-js-bundle' => false, 'no-html-minify' => false, 'refresh-content-version-only' => false, ], @@ -207,6 +208,7 @@ public function deployDataProvider() [ 'strategy' => 'compact', 'no-javascript' => false, + 'no-js-bundle' => false, 'no-html-minify' => false, 'refresh-content-version-only' => false, 'content-version' => '123456', @@ -226,25 +228,28 @@ public function deployDataProvider() public function testMaxExecutionTimeOptionPassed() { $options = [ - DeployStaticOptions::MAX_EXECUTION_TIME => 100, + DeployStaticOptions::MAX_EXECUTION_TIME => 100, DeployStaticOptions::REFRESH_CONTENT_VERSION_ONLY => false, - DeployStaticOptions::JOBS_AMOUNT => 3, - DeployStaticOptions::STRATEGY => 'compact', - DeployStaticOptions::NO_JAVASCRIPT => true, - DeployStaticOptions::NO_HTML_MINIFY => true, + DeployStaticOptions::JOBS_AMOUNT => 3, + DeployStaticOptions::STRATEGY => 'compact', + DeployStaticOptions::NO_JAVASCRIPT => true, + DeployStaticOptions::NO_JS_BUNDLE => true, + DeployStaticOptions::NO_HTML_MINIFY => true, ]; $queueMock = $this->createMock(Queue::class); $strategyMock = $this->createMock(CompactDeploy::class); $this->queueFactory->expects($this->once()) ->method('create') - ->with([ - 'logger' => $this->logger, - 'maxExecTime' => 100, - 'maxProcesses' => 3, - 'options' => $options, - 'deployPackageService' => null - ]) + ->with( + [ + 'logger' => $this->logger, + 'maxExecTime' => 100, + 'maxProcesses' => 3, + 'options' => $options, + 'deployPackageService' => null + ] + ) ->willReturn($queueMock); $this->deployStrategyFactory->expects($this->once()) ->method('create') diff --git a/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php b/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php index 2155efa017093..b35a3e68b0f20 100644 --- a/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php +++ b/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php @@ -10,6 +10,7 @@ use Magento\Developer\Model\Setup\Declaration\Schema\WhitelistGenerator; use Magento\Framework\Config\FileResolverByModule; use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\Console\Cli; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -80,11 +81,12 @@ protected function execute(InputInterface $input, OutputInterface $output) : int $this->whitelistGenerator->generate($moduleName); } catch (ConfigurationMismatchException $e) { $output->writeln($e->getMessage()); - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + return Cli::RETURN_FAILURE; } catch (\Exception $e) { - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + $output->writeln($e->getMessage()); + return Cli::RETURN_FAILURE; } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/Developer/Test/Unit/Console/Command/TablesWhitelistGenerateCommandTest.php b/app/code/Magento/Developer/Test/Unit/Console/Command/TablesWhitelistGenerateCommandTest.php new file mode 100644 index 0000000000000..8e547332c0b44 --- /dev/null +++ b/app/code/Magento/Developer/Test/Unit/Console/Command/TablesWhitelistGenerateCommandTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Developer\Test\Unit\Console\Command; + +use Magento\Developer\Console\Command\TablesWhitelistGenerateCommand as GenerateCommand; +use Magento\Developer\Model\Setup\Declaration\Schema\WhitelistGenerator; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\ConfigurationMismatchException as ConfigException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Class TablesWhitelistGenerateCommandTest + * + * @package Magento\Developer\Test\Unit\Console\Command + */ +class TablesWhitelistGenerateCommandTest extends TestCase +{ + // Exception Messages! + const CONFIG_EXCEPTION_MESSAGE = 'Configuration Exception Message'; + const EXCEPTION_MESSAGE = 'General Exception Message'; + + /** @var WhitelistGenerator|MockObject $whitelistGenerator */ + private $whitelistGenerator; + + /** @var GenerateCommand $instance */ + private $instance; + + protected function setUp() + { + $this->whitelistGenerator = $this->getMockBuilder(WhitelistGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->instance = new GenerateCommand($this->whitelistGenerator); + } + + /** + * Test case for success scenario + * + * @param string $arguments + * @param string $expected + * + * @dataProvider successDataProvider + */ + public function testCommandSuccess(string $arguments, string $expected) + { + $this->whitelistGenerator->expects($this->once()) + ->method('generate') + ->with($arguments); + + $commandTest = $this->execute($arguments); + $this->assertEquals($expected, $commandTest->getStatusCode()); + $this->assertEquals('', $commandTest->getDisplay()); + } + + /** + * Test case for failure scenario + * + * @param string $arguments + * @param string $expected + * @param \Exception|ConfigException $exception + * @param string $output + * + * @dataProvider failureDataProvider + */ + public function testCommandFailure(string $arguments, string $expected, $exception, string $output) + { + $this->whitelistGenerator->expects($this->once()) + ->method('generate') + ->with($arguments) + ->willReturnCallback( + function () use ($exception) { + throw $exception; + } + ); + + $commandTest = $this->execute($arguments); + $this->assertEquals($expected, $commandTest->getStatusCode()); + $this->assertEquals($output . PHP_EOL, $commandTest->getDisplay()); + } + + /** + * Data provider for success test case + * + * @return array + */ + public function successDataProvider() + { + return [ + [ + 'all', + Cli::RETURN_SUCCESS, + + ], + [ + 'Module_Name', + Cli::RETURN_SUCCESS + ] + ]; + } + + /** + * Data provider for failure test case + * + * @return array + */ + public function failureDataProvider() + { + return [ + [ + 'all', + Cli::RETURN_FAILURE, + new ConfigException(__('Configuration Exception Message')), + self::CONFIG_EXCEPTION_MESSAGE + ], + [ + 'Module_Name', + Cli::RETURN_FAILURE, + new ConfigException(__('Configuration Exception Message')), + self::CONFIG_EXCEPTION_MESSAGE + ], + [ + 'all', + Cli::RETURN_FAILURE, + new \Exception(self::EXCEPTION_MESSAGE), + self::EXCEPTION_MESSAGE + ], + [ + 'Module_Name', + Cli::RETURN_FAILURE, + new \Exception(self::EXCEPTION_MESSAGE), + self::EXCEPTION_MESSAGE + ] + ]; + } + + /** + * Execute command test class for symphony + * + * @param string $arguments + * + * @return CommandTester + */ + private function execute(string $arguments) + { + $commandTest = new CommandTester($this->instance); + $commandTest->execute(['--' . GenerateCommand::MODULE_NAME_KEY => $arguments]); + + return $commandTest; + } +} diff --git a/app/code/Magento/Developer/etc/adminhtml/system.xml b/app/code/Magento/Developer/etc/adminhtml/system.xml index c64abd6eae725..197dc6f981acf 100644 --- a/app/code/Magento/Developer/etc/adminhtml/system.xml +++ b/app/code/Magento/Developer/etc/adminhtml/system.xml @@ -11,7 +11,7 @@ <label>Frontend Development Workflow</label> <field id="type" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Workflow type</label> - <comment>Not available in production mode</comment> + <comment>Not available in production mode.</comment> <source_model>Magento\Developer\Model\Config\Source\WorkflowType</source_model> <frontend_model>Magento\Developer\Block\Adminhtml\System\Config\WorkflowType</frontend_model> <backend_model>Magento\Developer\Model\Config\Backend\WorkflowType</backend_model> diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 5959294fe6dc7..ad76f5070b35b 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -10,7 +10,6 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Async\CallbackDeferred; -use Magento\Framework\Async\ProxyDeferredFactory; use Magento\Framework\HTTP\AsyncClient\HttpResponseDeferredInterface; use Magento\Framework\HTTP\AsyncClient\Request; use Magento\Framework\HTTP\AsyncClientInterface; @@ -21,6 +20,7 @@ use Magento\Quote\Model\Quote\Address\RateResult\Error; use Magento\Shipping\Model\Carrier\AbstractCarrier; use Magento\Shipping\Model\Rate\Result; +use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory; use Magento\Framework\Xml\Security; use Magento\Dhl\Model\Validator\XmlValidator; @@ -389,16 +389,17 @@ public function collectRates(RateRequest $request) //Saving $result to use proper result with the callback $this->_result = $result = $this->_getQuotes(); //After quotes are loaded parsing the response. - return $this->proxyDeferredFactory->createFor( - Result::class, - new CallbackDeferred( - function () use ($request, $result) { - $this->_result = $result; - $this->_updateFreeMethodQuote($request); - - return $this->_result; - } - ) + return $this->proxyDeferredFactory->create( + [ + 'deferred' => new CallbackDeferred( + function () use ($request, $result) { + $this->_result = $result; + $this->_updateFreeMethodQuote($request); + + return $this->_result; + } + ) + ] ); } @@ -818,16 +819,16 @@ protected function _getAllItems() if (!empty($decimalItems)) { foreach ($decimalItems as $decimalItem) { - $fullItems = array_merge( - $fullItems, - array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight']) - ); + $fullItems[] = array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight']); } } else { - $fullItems = array_merge($fullItems, array_fill(0, $qty, $this->_getWeight($itemWeight))); + $fullItems[] = array_fill(0, $qty, $this->_getWeight($itemWeight)); } } - sort($fullItems); + if ($fullItems) { + $fullItems = array_merge(...$fullItems); + sort($fullItems); + } return $fullItems; } @@ -939,7 +940,7 @@ protected function _getDimension($dimension, $configWeightUnit = false) ); } - return sprintf('%.3f', $dimension); + return round($dimension, 3); } /** @@ -1057,23 +1058,24 @@ protected function _getQuotes() } } - return $this->proxyDeferredFactory->createFor( - Result::class, - new CallbackDeferred( - function () use ($deferredResponses, $responseBodies) { - //Loading rates not found in cache - foreach ($deferredResponses as $deferredResponseData) { - $responseBodies[] = [ - 'body' => $deferredResponseData['deferred']->get()->getBody(), - 'date' => $deferredResponseData['date'], - 'request' => $deferredResponseData['request'], - 'from_cache' => false - ]; - } + return $this->proxyDeferredFactory->create( + [ + 'deferred' => new CallbackDeferred( + function () use ($deferredResponses, $responseBodies) { + //Loading rates not found in cache + foreach ($deferredResponses as $deferredResponseData) { + $responseBodies[] = [ + 'body' => $deferredResponseData['deferred']->get()->getBody(), + 'date' => $deferredResponseData['date'], + 'request' => $deferredResponseData['request'], + 'from_cache' => false + ]; + } - return $this->processQuotesResponses($responseBodies); - } - ) + return $this->processQuotesResponses($responseBodies); + } + ) + ] ); } diff --git a/app/code/Magento/Dhl/Test/Mftf/Section/AdminShippingMethodDHLSection.xml b/app/code/Magento/Dhl/Test/Mftf/Section/AdminShippingMethodDHLSection.xml new file mode 100644 index 0000000000000..1256bb5443e04 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Mftf/Section/AdminShippingMethodDHLSection.xml @@ -0,0 +1,30 @@ +<?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="AdminShippingMethodDHLSection"> + <element name="carriersDHLTab" type="button" selector="#carriers_dhl-head"/> + <element name="carriersDHLActive" type="input" selector="#carriers_dhl_active_inherit"/> + <element name="carriersDHLTitle" type="input" selector="#carriers_dhl_title_inherit"/> + <element name="carriersDHLAccessId" type="input" selector="#carriers_dhl_id"/> + <element name="carriersDHLPassword" type="input" selector="#carriers_dhl_password"/> + <element name="carriersDHLAccount" type="input" selector="#carriers_dhl_account_inherit"/> + <element name="carriersDHLContentType" type="input" selector="#carriers_dhl_content_type_inherit"/> + <element name="carriersDHLHandlingType" type="input" selector="#carriers_dhl_handling_type_inherit"/> + <element name="carriersDHLHandlingAction" type="input" selector="#carriers_dhl_handling_action_inherit"/> + <element name="carriersDHLDivideOrderWeight" type="input" selector="#carriers_dhl_divide_order_weight_inherit"/> + <element name="carriersDHLUnitOfMeasure" type="input" selector="#carriers_dhl_unit_of_measure_inherit"/> + <element name="carriersDHLSize" type="input" selector="#carriers_dhl_size_inherit"/> + <element name="carriersDHLNonDocAllowedMethod" type="input" selector="#carriers_dhl_nondoc_methods_inherit"/> + <element name="carriersDHLSmartPostHubId" type="input" selector="#carriers_dhl_doc_methods_inherit"/> + <element name="carriersDHLSpecificErrMsg" type="input" selector="#carriers_dhl_specificerrmsg_inherit"/> + <element name="carriersDHLAllowSpecific" type="input" selector="#carriers_dhl_sallowspecific_inherit"/> + <element name="carriersDHLSpecificCountry" type="input" selector="#carriers_dhl_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Dhl/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Dhl/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..f5e1e8ef0c8ec --- /dev/null +++ b/app/code/Magento/Dhl/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in DHL section--> + <comment userInput="Assert configuration are disabled in DHL section" stepKey="commentSeeDisabledDHLConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodDHLSection.carriersDHLTab}}" dependentSelector="{{AdminShippingMethodDHLSection.carriersDHLActive}}" visible="false" stepKey="expandDHLTab"/> + <waitForElementVisible selector="{{AdminShippingMethodDHLSection.carriersDHLActive}}" stepKey="waitDHLTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLActive}}" userInput="disabled" stepKey="grabDHLActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLActiveDisabled" stepKey="assertDHLActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLTitle}}" userInput="disabled" stepKey="grabDHLTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLTitleDisabled" stepKey="assertDHLTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLAccessId}}" userInput="disabled" stepKey="grabDHLAccessIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLAccessIdDisabled" stepKey="assertDHLAccessIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLPassword}}" userInput="disabled" stepKey="grabDHLPasswordDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLPasswordDisabled" stepKey="assertDHLPasswordDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLAccount}}" userInput="disabled" stepKey="grabDHLAccountDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLAccountDisabled" stepKey="assertDHLAccountDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLContentType}}" userInput="disabled" stepKey="grabDHLContentTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLContentTypeDisabled" stepKey="assertDHLContentTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLHandlingType}}" userInput="disabled" stepKey="grabDHLHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLHandlingTypeDisabled" stepKey="assertDHLHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLHandlingAction}}" userInput="disabled" stepKey="grabDHLHandlingDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLHandlingDisabled" stepKey="assertDHLHandlingDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLDivideOrderWeight}}" userInput="disabled" stepKey="grabDHLDivideOrderWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLDivideOrderWeightDisabled" stepKey="assertDHLDivideOrderWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLUnitOfMeasure}}" userInput="disabled" stepKey="grabDHLUnitOfMeasureDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLUnitOfMeasureDisabled" stepKey="assertDHLUnitOfMeasureDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLSize}}" userInput="disabled" stepKey="grabDHLSizeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLSizeDisabled" stepKey="assertDHLSizeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLNonDocAllowedMethod}}" userInput="disabled" stepKey="grabDHLNonDocAllowedMethodDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLNonDocAllowedMethodDisabled" stepKey="assertDHLNonDocAllowedMethodDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLSmartPostHubId}}" userInput="disabled" stepKey="grabDHLSmartPostHubIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLSmartPostHubIdDisabled" stepKey="assertDHLSmartPostHubIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLSpecificErrMsg}}" userInput="disabled" stepKey="grabDHLSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLSpecificErrMsgDisabled" stepKey="assertDHLSpecificErrMsgDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index ea3da2ca031a5..93a16821e385d 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -88,7 +88,7 @@ </field> <field id="ready_time" translate="label comment" type="text" sortOrder="180" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Ready time</label> - <comment>Package ready time after order submission (in hours)</comment> + <comment>Package ready time after order submission (in hours).</comment> </field> <field id="specificerrmsg" translate="label" type="textarea" sortOrder="800" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Displayed Error Message</label> diff --git a/app/code/Magento/Dhl/etc/config.xml b/app/code/Magento/Dhl/etc/config.xml index b46152fb0ecad..3408447e70650 100644 --- a/app/code/Magento/Dhl/etc/config.xml +++ b/app/code/Magento/Dhl/etc/config.xml @@ -32,7 +32,7 @@ <specificerrmsg>This shipping method is currently unavailable. If you would like to ship using this shipping method, please contact us.</specificerrmsg> <divide_order_weight>1</divide_order_weight> <unit_of_measure>K</unit_of_measure> - <size>R</size> + <size>0</size> <handling_type>F</handling_type> <handling_action>O</handling_action> <shipment_days>Mon,Tue,Wed,Thu,Fri</shipment_days> diff --git a/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php b/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php index 8b9e0b1be7df2..8e27b9905ed6f 100644 --- a/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php +++ b/app/code/Magento/Directory/Model/Currency/Import/CurrencyConverterApi.php @@ -7,43 +7,57 @@ namespace Magento\Directory\Model\Currency\Import; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Exception; + /** - * Currency rate import model (From http://free.currencyconverterapi.com/) - * - * Class \Magento\Directory\Model\Currency\Import\CurrencyConverterApi + * Currency rate converter (free.currconv.com). */ class CurrencyConverterApi extends AbstractImport { /** * @var string */ - const CURRENCY_CONVERTER_URL = 'http://free.currencyconverterapi.com/api/v3/convert?q={{CURRENCY_FROM}}_{{CURRENCY_TO}}&compact=ultra&apiKey={{API_KEY}}'; //@codingStandardsIgnoreLine + public const CURRENCY_CONVERTER_URL = 'https://free.currconv.com/api/v7/convert?apiKey={{ACCESS_KEY}}' + . '&q={{CURRENCY_RATES}}&compact=ultra'; /** * Http Client Factory * - * @var \Magento\Framework\HTTP\ZendClientFactory + * @var ZendClientFactory */ private $httpClientFactory; /** * Core scope config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfig */ private $scopeConfig; /** - * Initialize dependencies - * - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory + * @var string + */ + private $currencyConverterServiceHost = ''; + + /** + * @var string + */ + private $serviceUrl = ''; + + /** + * @param CurrencyFactory $currencyFactory + * @param ScopeConfig $scopeConfig + * @param ZendClientFactory $httpClientFactory */ public function __construct( - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory + CurrencyFactory $currencyFactory, + ScopeConfig $scopeConfig, + ZendClientFactory $httpClientFactory ) { parent::__construct($currencyFactory); $this->scopeConfig = $scopeConfig; @@ -77,30 +91,39 @@ public function fetchRates() * @param array $currenciesTo * @return array */ - private function convertBatch($data, $currencyFrom, $currenciesTo) + private function convertBatch(array $data, string $currencyFrom, array $currenciesTo): array { - $apiKey = $this->scopeConfig->getValue( - 'currency/currencyconverterapi/api_key', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - if (!$apiKey) { - $this->_messages[] = __('No API Key was specified.'); + $url = $this->getServiceURL($currencyFrom, $currenciesTo); + if (empty($url)) { + $data[$currencyFrom] = $this->makeEmptyResponse($currenciesTo); return $data; } + // phpcs:ignore Magento2.Functions.DiscouragedFunction + set_time_limit(0); + try { + $response = $this->getServiceResponse($url); + } finally { + ini_restore('max_execution_time'); + } + + if (!$this->validateResponse($response)) { + $data[$currencyFrom] = $this->makeEmptyResponse($currenciesTo); + return $data; + } + foreach ($currenciesTo as $to) { - //phpcs:ignore Magento2.Functions.DiscouragedFunction - set_time_limit(0); - try { - $url = str_replace('{{CURRENCY_FROM}}', $currencyFrom, self::CURRENCY_CONVERTER_URL); - $url = str_replace('{{CURRENCY_TO}}', $to, $url); - $url = str_replace('{{API_KEY}}', $apiKey, $url); - if ($currencyFrom == $to) { - $data[$currencyFrom][$to] = $this->_numberFormat(1); + if ($currencyFrom === $to) { + $data[$currencyFrom][$to] = $this->_numberFormat(1); + } else { + if (!isset($response[$currencyFrom . '_' . $to])) { + $serviceHost = $this->getServiceHost($url); + $this->_messages[] = __('We can\'t retrieve a rate from %1 for %2.', $serviceHost, $to); + $data[$currencyFrom][$to] = null; } else { - $data[$currencyFrom][$to] = $this->getCurrencyRate($currencyFrom, $to, $url); + $data[$currencyFrom][$to] = $this->_numberFormat( + (double)$response[$currencyFrom . '_' . $to] + ); } - } finally { - ini_restore('max_execution_time'); } } @@ -108,33 +131,52 @@ private function convertBatch($data, $currencyFrom, $currenciesTo) } /** - * Get currency rate from api + * Get currency converter service host. * - * @param string $currencyFrom - * @param string $to * @param string $url - * @return double + * @return string */ - private function getCurrencyRate($currencyFrom, $to, $url) + private function getServiceHost(string $url): string { - $rate = null; - $response = $this->getServiceResponse($url); - if (empty($response)) { - $this->_messages[] = __('We can\'t retrieve a rate from %1 for %2.', $url, $to); - $rate = null; - } else { - if (isset($response['error']) && $response['error']) { - if (!in_array($response['error'], $this->_messages)) { - $this->_messages[] = $response['error']; - } - $rate = null; - } else { - $rate = $this->_numberFormat( - (double)$response[$currencyFrom . '_' . $to] - ); + if (!$this->currencyConverterServiceHost) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $this->currencyConverterServiceHost = parse_url($url, PHP_URL_SCHEME) . '://' + // phpcs:ignore Magento2.Functions.DiscouragedFunction + . parse_url($url, PHP_URL_HOST); + } + return $this->currencyConverterServiceHost; + } + + /** + * Return service URL. + * + * @param string $currencyFrom + * @param array $currenciesTo + * @return string + */ + private function getServiceURL(string $currencyFrom, array $currenciesTo): string + { + if (!$this->serviceUrl) { + // Get access key + $accessKey = $this->scopeConfig + ->getValue('currency/currencyconverterapi/api_key', ScopeInterface::SCOPE_STORE); + if (empty($accessKey)) { + $this->_messages[] = __('No API Key was specified or an invalid API Key was specified.'); + return ''; } + // Get currency rates request + $currencyQueryParts = []; + foreach ($currenciesTo as $currencyTo) { + $currencyQueryParts[] = sprintf('%s_%s', $currencyFrom, $currencyTo); + } + $currencyRates = implode(',', $currencyQueryParts); + $this->serviceUrl = str_replace( + ['{{ACCESS_KEY}}', '{{CURRENCY_RATES}}'], + [$accessKey, $currencyRates], + self::CURRENCY_CONVERTER_URL + ); } - return $rate; + return $this->serviceUrl; } /** @@ -144,9 +186,9 @@ private function getCurrencyRate($currencyFrom, $to, $url) * @param int $retry * @return array */ - private function getServiceResponse($url, $retry = 0) + private function getServiceResponse($url, $retry = 0): array { - /** @var \Magento\Framework\HTTP\ZendClient $httpClient */ + /** @var ZendClient $httpClient */ $httpClient = $this->httpClientFactory->create(); $response = []; @@ -157,15 +199,15 @@ private function getServiceResponse($url, $retry = 0) [ 'timeout' => $this->scopeConfig->getValue( 'currency/currencyconverterapi/timeout', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ), ] )->request( 'GET' )->getBody(); - $response = json_decode($jsonResponse, true); - } catch (\Exception $e) { + $response = json_decode($jsonResponse, true) ?: []; + } catch (Exception $e) { if ($retry == 0) { $response = $this->getServiceResponse($url, 1); } @@ -173,6 +215,32 @@ private function getServiceResponse($url, $retry = 0) return $response; } + /** + * Validate rates response. + * + * @param array $response + * @return bool + */ + private function validateResponse(array $response): bool + { + if (!isset($response['error'])) { + return true; + } + $this->_messages[] = $response['error'] ?: __('Currency rates can\'t be retrieved.'); + return false; + } + + /** + * Make empty rates for provided currencies. + * + * @param array $currenciesTo + * @return array + */ + private function makeEmptyResponse(array $currenciesTo): array + { + return array_fill_keys($currenciesTo, null); + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Directory/Model/Currency/Import/FixerIo.php b/app/code/Magento/Directory/Model/Currency/Import/FixerIo.php index af49d6daaf379..29cdbabb9b993 100644 --- a/app/code/Magento/Directory/Model/Currency/Import/FixerIo.php +++ b/app/code/Magento/Directory/Model/Currency/Import/FixerIo.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Directory\Model\Currency\Import; use Magento\Store\Model\ScopeInterface; @@ -32,6 +34,11 @@ class FixerIo extends AbstractImport */ private $scopeConfig; + /** + * @var string + */ + private $currencyConverterServiceHost = ''; + /** * Initialize dependencies * @@ -73,6 +80,7 @@ public function fetchRates() */ protected function _convert($currencyFrom, $currencyTo) { + return 1; } /** @@ -98,7 +106,7 @@ private function convertBatch(array $data, string $currencyFrom, array $currenci [$accessKey, $currencyFrom, $currenciesStr], self::CURRENCY_CONVERTER_URL ); - + // phpcs:ignore Magento2.Functions.DiscouragedFunction set_time_limit(0); try { $response = $this->getServiceResponse($url); @@ -116,7 +124,8 @@ private function convertBatch(array $data, string $currencyFrom, array $currenci $data[$currencyFrom][$currencyTo] = $this->_numberFormat(1); } else { if (empty($response['rates'][$currencyTo])) { - $this->_messages[] = __('We can\'t retrieve a rate from %1 for %2.', $url, $currencyTo); + $serviceHost = $this->getServiceHost($url); + $this->_messages[] = __('We can\'t retrieve a rate from %1 for %2.', $serviceHost, $currencyTo); $data[$currencyFrom][$currencyTo] = null; } else { $data[$currencyFrom][$currencyTo] = $this->_numberFormat( @@ -198,4 +207,21 @@ private function makeEmptyResponse(array $currenciesTo): array { return array_fill_keys($currenciesTo, null); } + + /** + * Get currency converter service host. + * + * @param string $url + * @return string + */ + private function getServiceHost(string $url): string + { + if (!$this->currencyConverterServiceHost) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $this->currencyConverterServiceHost = parse_url($url, PHP_URL_SCHEME) . '://' + // phpcs:ignore Magento2.Functions.DiscouragedFunction + . parse_url($url, PHP_URL_HOST); + } + return $this->currencyConverterServiceHost; + } } diff --git a/app/code/Magento/Directory/Model/Observer.php b/app/code/Magento/Directory/Model/Observer.php index e35c2de5cee5b..6d227e7148261 100644 --- a/app/code/Magento/Directory/Model/Observer.php +++ b/app/code/Magento/Directory/Model/Observer.php @@ -12,6 +12,11 @@ namespace Magento\Directory\Model; +/** + * Class Observer + * + * @package Magento\Directory\Model + */ class Observer { const CRON_STRING_PATH = 'crontab/default/jobs/currency_rates_update/schedule/cron_expr'; @@ -83,6 +88,8 @@ public function __construct( } /** + * Schedule update currency rates + * * @param mixed $schedule * @return void * @throws \Exception @@ -122,7 +129,7 @@ public function scheduledUpdateCurrencyRates($schedule) $importWarnings[] = __('FATAL ERROR:') . ' ' . __('Please specify the correct Import Service.'); } - if (sizeof($errors) > 0) { + if (count($errors) > 0) { foreach ($errors as $error) { $importWarnings[] = __('WARNING:') . ' ' . $error; } @@ -132,7 +139,7 @@ public function scheduledUpdateCurrencyRates($schedule) self::XML_PATH_ERROR_RECIPIENT, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - if (sizeof($importWarnings) == 0) { + if (count($importWarnings) == 0) { $this->_currencyFactory->create()->saveRates($rates); } elseif ($errorRecipient) { //if $errorRecipient is not set, there is no sense send email to nobody diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index 5339b0c9eb5bd..5db880c00343a 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -138,7 +138,7 @@ public function getAnyRate($currencyFrom, $currencyTo) */ public function saveRates($rates) { - if (is_array($rates) && sizeof($rates) > 0) { + if (is_array($rates) && count($rates) > 0) { $connection = $this->getConnection(); $data = []; foreach ($rates as $currencyCode => $rate) { @@ -176,7 +176,7 @@ public function getConfigCurrencies($model, $path) $result = []; $rowSet = $connection->fetchAll($select, $bind); foreach ($rowSet as $row) { - $result = array_merge($result, explode(',', $row['value'])); + $result[] = explode(',', $row['value']); } sort($result); diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddCountriesCaribbeanCuracaoKosovoSintMaarten.php b/app/code/Magento/Directory/Setup/Patch/Data/AddCountriesCaribbeanCuracaoKosovoSintMaarten.php new file mode 100644 index 0000000000000..d7bec84e5440c --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddCountriesCaribbeanCuracaoKosovoSintMaarten.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Class AddCountriesCaribbeanCuracaoKosovoSintMaarten + * + * @package Magento\Directory\Setup\Patch + */ +class AddCountriesCaribbeanCuracaoKosovoSintMaarten implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * AddCountriesCaribbeanCuracaoKosovoSintMaarten constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** + * Fill table directory/country + */ + $data = [ + [ + 'country_id' => 'BQ', + 'iso2_code' => 'BQ', + 'iso3_code' => 'BES', + ], + [ + 'country_id' => 'CW', + 'iso2_code' => 'CW', + 'iso3_code' => 'CUW', + ], + [ + 'country_id' => 'SX', + 'iso2_code' => 'SX', + 'iso3_code' => 'SXM', + ], + [ + 'country_id' => 'XK', + 'iso2_code' => 'XK', + 'iso3_code' => 'XKX', + ], + ]; + + $this->moduleDataSetup->getConnection()->insertOnDuplicate( + $this->moduleDataSetup->getTable('directory_country'), + $data + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php new file mode 100644 index 0000000000000..fa5fff3486a99 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add Regions for Belgium. + */ +class AddDataForBelgium implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForBelgium() + ); + } + + /** + * Belgium states data. + * + * @return array + */ + private function getDataForBelgium() + { + return [ + ['BE', 'VAN', 'Antwerpen'], + ['BE', 'WBR', 'Brabant wallon'], + ['BE', 'BRU', 'Brussels-Capital Region'], + ['BE', 'WHT', 'Hainaut'], + ['BE', 'VLI', 'Limburg'], + ['BE', 'WLG', 'Liège'], + ['BE', 'WLX', 'Luxembourg'], + ['BE', 'WNA', 'Namur'], + ['BE', 'VOV', 'Oost-Vlaanderen'], + ['BE', 'VBR', 'Vlaams-Brabant'], + ['BE', 'VWV', 'West-Vlaanderen'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php new file mode 100644 index 0000000000000..0750f8056c4d7 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add China States + */ +class AddDataForChina implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForChina() + ); + } + + /** + * China states data. + * + * @return array + */ + private function getDataForChina() + { + return [ + ['CN', 'CN-AH', 'Anhui Sheng'], + ['CN', 'CN-BJ', 'Beijing Shi'], + ['CN', 'CN-CQ', 'Chongqing Shi'], + ['CN', 'CN-FJ', 'Fujian Sheng'], + ['CN', 'CN-GS', 'Gansu Sheng'], + ['CN', 'CN-GD', 'Guangdong Sheng'], + ['CN', 'CN-GX', 'Guangxi Zhuangzu Zizhiqu'], + ['CN', 'CN-GZ', 'Guizhou Sheng'], + ['CN', 'CN-HI', 'Hainan Sheng'], + ['CN', 'CN-HE', 'Hebei Sheng'], + ['CN', 'CN-HL', 'Heilongjiang Sheng'], + ['CN', 'CN-HA', 'Henan Sheng'], + ['CN', 'CN-HK', 'Hong Kong SAR'], + ['CN', 'CN-HB', 'Hubei Sheng'], + ['CN', 'CN-HN', 'Hunan Sheng'], + ['CN', 'CN-JS', 'Jiangsu Sheng'], + ['CN', 'CN-JX', 'Jiangxi Sheng'], + ['CN', 'CN-JL', 'Jilin Sheng'], + ['CN', 'CN-LN', 'Liaoning Sheng'], + ['CN', 'CN-MO', 'Macao SAR'], + ['CN', 'CN-NM', 'Nei Mongol Zizhiqu'], + ['CN', 'CN-NX', 'Ningxia Huizi Zizhiqu'], + ['CN', 'CN-QH', 'Qinghai Sheng'], + ['CN', 'CN-SN', 'Shaanxi Sheng'], + ['CN', 'CN-SD', 'Shandong Sheng'], + ['CN', 'CN-SH', 'Shanghai Shi'], + ['CN', 'CN-SX', 'Shanxi Sheng'], + ['CN', 'CN-SC', 'Sichuan Sheng'], + ['CN', 'CN-TW', 'Taiwan Sheng'], + ['CN', 'CN-TJ', 'Tianjin Shi'], + ['CN', 'CN-XJ', 'Xinjiang Uygur Zizhiqu'], + ['CN', 'CN-XZ', 'Xizang Zizhiqu'], + ['CN', 'CN-YN', 'Yunnan Sheng'], + ['CN', 'CN-ZJ', 'Zhejiang Sheng'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForColombia.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForColombia.php new file mode 100644 index 0000000000000..38a9759828588 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForColombia.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Class AddDataForColombia + */ +class AddDataForColombia implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForColombia() + ); + } + + /** + * Colombia states data. + * + * @return array + */ + private function getDataForColombia() + { + return [ + ['CO', 'CO-AMA', 'Amazonas'], + ['CO', 'CO-ANT', 'Antioquia'], + ['CO', 'CO-ARA', 'Arauca'], + ['CO', 'CO-ATL', 'Atlántico'], + ['CO', 'CO-BOL', 'Bolívar'], + ['CO', 'CO-BOY', 'Boyacá'], + ['CO', 'CO-CAL', 'Caldas'], + ['CO', 'CO-CAQ', 'Caquetá'], + ['CO', 'CO-CAS', 'Casanare'], + ['CO', 'CO-CAU', 'Cauca'], + ['CO', 'CO-CES', 'Cesar'], + ['CO', 'CO-CHO', 'Chocó'], + ['CO', 'CO-COR', 'Córdoba'], + ['CO', 'CO-CUN', 'Cundinamarca'], + ['CO', 'CO-GUA', 'Guainía'], + ['CO', 'CO-GUV', 'Guaviare'], + ['CO', 'CO-HUL', 'Huila'], + ['CO', 'CO-LAG', 'La Guajira'], + ['CO', 'CO-MAG', 'Magdalena'], + ['CO', 'CO-MET', 'Meta'], + ['CO', 'CO-NAR', 'Nariño'], + ['CO', 'CO-NSA', 'Norte de Santander'], + ['CO', 'CO-PUT', 'Putumayo'], + ['CO', 'CO-QUI', 'Quindío'], + ['CO', 'CO-RIS', 'Risaralda'], + ['CO', 'CO-SAP', 'San Andrés y Providencia'], + ['CO', 'CO-SAN', 'Santander'], + ['CO', 'CO-SUC', 'Sucre'], + ['CO', 'CO-TOL', 'Tolima'], + ['CO', 'CO-VAC', 'Valle del Cauca'], + ['CO', 'CO-VAU', 'Vaupés'], + ['CO', 'CO-VID', 'Vichada'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForPoland.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForPoland.php new file mode 100644 index 0000000000000..c5ab4cd186286 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForPoland.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See PLPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Directory\Setup\DataInstallerFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add Poland States + */ +class AddDataForPoland implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForPoland() + ); + } + + /** + * Poland states data. + * + * @return array + */ + private function getDataForPoland() + { + return [ + ['PL', 'PL-02', 'dolnośląskie'], + ['PL', 'PL-04', 'kujawsko-pomorskie'], + ['PL', 'PL-06', 'lubelskie'], + ['PL', 'PL-08', 'lubuskie'], + ['PL', 'PL-10', 'łódzkie'], + ['PL', 'PL-12', 'małopolskie'], + ['PL', 'PL-14', 'mazowieckie'], + ['PL', 'PL-16', 'opolskie'], + ['PL', 'PL-18', 'podkarpackie'], + ['PL', 'PL-20', 'podlaskie'], + ['PL', 'PL-22', 'pomorskie'], + ['PL', 'PL-24', 'śląskie'], + ['PL', 'PL-26', 'świętokrzyskie'], + ['PL', 'PL-28', 'warmińsko-mazurskie'], + ['PL', 'PL-30', 'wielkopolskie'], + ['PL', 'PL-32', 'zachodniopomorskie'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Test/Mftf/Data/CurrencyConfigData.xml b/app/code/Magento/Directory/Test/Mftf/Data/CurrencyConfigData.xml new file mode 100644 index 0000000000000..fb21ee42fe2fc --- /dev/null +++ b/app/code/Magento/Directory/Test/Mftf/Data/CurrencyConfigData.xml @@ -0,0 +1,16 @@ +<?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="BaseCurrencyRUBConfigData"> + <data key="path">currency/options/base</data> + <data key="label">Russian Ruble</data> + <data key="value">RUB</data> + </entity> +</entities> diff --git a/app/code/Magento/Directory/Test/Mftf/Page/AdminCurrencySetupPage.xml b/app/code/Magento/Directory/Test/Mftf/Page/AdminCurrencySetupPage.xml new file mode 100644 index 0000000000000..03c2b0f856d19 --- /dev/null +++ b/app/code/Magento/Directory/Test/Mftf/Page/AdminCurrencySetupPage.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="AdminCurrencySetupPage" url="admin/system_config/edit/section/currency/" area="admin" module="Magento_Directory"> + <section name="AdminScheduledImportSettingsSection"/> + </page> +</pages> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml b/app/code/Magento/Directory/Test/Mftf/Section/AdminScheduledImportSettingsSection.xml similarity index 52% rename from app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml rename to app/code/Magento/Directory/Test/Mftf/Section/AdminScheduledImportSettingsSection.xml index f9f1bef38d17d..4c4b0d6c9541e 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml +++ b/app/code/Magento/Directory/Test/Mftf/Section/AdminScheduledImportSettingsSection.xml @@ -8,8 +8,9 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StoresConfigurationListSection"> - <element name="sales" type="button" selector="#system_config_tabs > div:nth-child(4) > div"/> - <element name="salesPaymentMethods" type="button" selector="//a[contains(@class, 'item-nav')]//span[text()='Payment Methods']"/> + <section name="AdminScheduledImportSettingsSection"> + <element name="head" type="button" selector="#currency_import-head"/> + <element name="enabled" type="input" selector="#currency_import_enabled"/> + <element name="service" type="input" selector="#currency_import_service"/> </section> </sections> diff --git a/app/code/Magento/Directory/Test/Mftf/Test/AdminScheduledImportSettingsHiddenTest.xml b/app/code/Magento/Directory/Test/Mftf/Test/AdminScheduledImportSettingsHiddenTest.xml new file mode 100644 index 0000000000000..0320b6f422cd6 --- /dev/null +++ b/app/code/Magento/Directory/Test/Mftf/Test/AdminScheduledImportSettingsHiddenTest.xml @@ -0,0 +1,33 @@ +<?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="AdminScheduledImportSettingsHiddenTest"> + <annotations> + <features value="Directory"/> + <title value="Scheduled import settings hidden" /> + <description value="Scheduled Import Settings' should hide fields when 'Enabled' is 'No'"/> + <severity value="MINOR"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set currency/import/enabled 1" stepKey="enableCurrencyImport"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="config:set currency/import/enabled 0" stepKey="disableCurrencyImport"/> + </after> + + <amOnPage url="{{AdminCurrencySetupPage.url}}" stepKey="openCurrencyOptionsPage" /> + <conditionalClick dependentSelector="{{AdminScheduledImportSettingsSection.enabled}}" visible="false" selector="{{AdminScheduledImportSettingsSection.head}}" stepKey="openCollapsibleBlock"/> + <see selector="{{AdminScheduledImportSettingsSection.service}}" userInput="Fixer.io" stepKey="seeServiceFixerIo"/> + <selectOption selector="{{AdminScheduledImportSettingsSection.enabled}}" userInput="0" stepKey="disableCurrencyImportOption"/> + <dontSeeElement selector="{{AdminScheduledImportSettingsSection.service}}" stepKey="dontSeeServiceFixerIo"/> + </test> +</tests> diff --git a/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/CurrencyConverterApiTest.php b/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/CurrencyConverterApiTest.php new file mode 100644 index 0000000000000..797f7b73f33be --- /dev/null +++ b/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/CurrencyConverterApiTest.php @@ -0,0 +1,193 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Directory\Test\Unit\Model\Currency\Import; + +use Magento\Directory\Model\Currency; +use Magento\Directory\Model\Currency\Import\CurrencyConverterApi; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\TestCase; + +/** + * CurrencyConverterApi converter test. + */ +class CurrencyConverterApiTest extends TestCase +{ + /** + * @var CurrencyConverterApi + */ + private $model; + + /** + * @var CurrencyFactory|MockObject + */ + private $currencyFactory; + + /** + * @var ZendClientFactory|MockObject + */ + private $httpClientFactory; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->currencyFactory = $this->getMockBuilder(CurrencyFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->httpClientFactory = $this->getMockBuilder(ZendClientFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + CurrencyConverterApi::class, + [ + 'currencyFactory' => $this->currencyFactory, + 'scopeConfig' => $this->scopeConfig, + 'httpClientFactory' => $this->httpClientFactory, + ] + ); + } + + /** + * Prepare CurrencyFactory mock. + */ + private function prepareCurrencyFactoryMock(): void + { + $currencyFromList = ['USD']; + $currencyToList = ['EUR', 'UAH']; + + /** @var Currency|MockObject $currency */ + $currency = $this->getMockBuilder(Currency::class)->disableOriginalConstructor()->getMock(); + $currency->expects($this->once())->method('getConfigBaseCurrencies')->willReturn($currencyFromList); + $currency->expects($this->once())->method('getConfigAllowCurrencies')->willReturn($currencyToList); + + $this->currencyFactory->expects($this->atLeastOnce())->method('create')->willReturn($currency); + } + + /** + * Prepare FetchRates test. + * + * @param string $responseBody + */ + private function prepareFetchRatesTest(string $responseBody): void + { + $this->prepareCurrencyFactoryMock(); + + $this->scopeConfig->method('getValue') + ->withConsecutive( + ['currency/currencyconverterapi/api_key', 'store'], + ['currency/currencyconverterapi/timeout', 'store'] + ) + ->willReturnOnConsecutiveCalls('api_key', 100); + + /** @var ZendClient|MockObject $httpClient */ + $httpClient = $this->getMockBuilder(ZendClient::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var DataObject|MockObject $currencyMock */ + $httpResponse = $this->getMockBuilder(DataObject::class) + ->disableOriginalConstructor() + ->setMethods(['getBody']) + ->getMock(); + + $this->httpClientFactory->expects($this->once())->method('create')->willReturn($httpClient); + $httpClient->expects($this->once())->method('setUri')->willReturnSelf(); + $httpClient->expects($this->once())->method('setConfig')->willReturnSelf(); + $httpClient->expects($this->once())->method('request')->willReturn($httpResponse); + $httpResponse->expects($this->once())->method('getBody')->willReturn($responseBody); + } + + /** + * Test Fetch Rates + * + * @return void + */ + public function testFetchRates(): void + { + $expectedCurrencyRateList = ['USD' => ['EUR' => 0.891285, 'UAH' => 26.16]]; + $responseBody = '{"USD_EUR":0.891285,"USD_UAH":26.16,"USD_USD":1}'; + $this->prepareFetchRatesTest($responseBody); + + self::assertEquals($expectedCurrencyRateList, $this->model->fetchRates()); + } + + /** + * Test FetchRates when Service Response is empty. + */ + public function testFetchRatesWhenServiceResponseIsEmpty(): void + { + $responseBody = ''; + $expectedCurrencyRateList = ['USD' => ['EUR' => null, 'UAH' => null]]; + $cantRetrieveCurrencyMessage = "We can't retrieve a rate from " + . "https://free.currconv.com for %s."; + $this->prepareFetchRatesTest($responseBody); + + self::assertEquals($expectedCurrencyRateList, $this->model->fetchRates()); + + $messages = $this->model->getMessages(); + self::assertEquals(sprintf($cantRetrieveCurrencyMessage, 'EUR'), (string) $messages[0]); + self::assertEquals(sprintf($cantRetrieveCurrencyMessage, 'UAH'), (string) $messages[1]); + } + + /** + * Test FetchRates when Service Response has error. + */ + public function testFetchRatesWhenServiceResponseHasError(): void + { + $serviceErrorMessage = 'Service error'; + $responseBody = sprintf('{"error":"%s"}', $serviceErrorMessage); + $expectedCurrencyRateList = ['USD' => ['EUR' => null, 'UAH' => null]]; + $this->prepareFetchRatesTest($responseBody); + + self::assertEquals($expectedCurrencyRateList, $this->model->fetchRates()); + + $messages = $this->model->getMessages(); + self::assertEquals($serviceErrorMessage, (string) $messages[0]); + } + + /** + * Test FetchRates when Service URL is empty. + */ + public function testFetchRatesWhenServiceUrlIsEmpty(): void + { + $this->prepareCurrencyFactoryMock(); + + $this->scopeConfig->method('getValue') + ->withConsecutive( + ['currency/currencyconverterapi/api_key', 'store'], + ['currency/currencyconverterapi/timeout', 'store'] + ) + ->willReturnOnConsecutiveCalls('', 100); + + $expectedCurrencyRateList = ['USD' => ['EUR' => null, 'UAH' => null]]; + self::assertEquals($expectedCurrencyRateList, $this->model->fetchRates()); + + $noApiKeyErrorMessage = 'No API Key was specified or an invalid API Key was specified.'; + $messages = $this->model->getMessages(); + self::assertEquals($noApiKeyErrorMessage, (string) $messages[0]); + } +} diff --git a/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/FixerIoTest.php b/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/FixerIoTest.php index 7efcf0d62712a..3147ac6ca2b91 100644 --- a/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/FixerIoTest.php +++ b/app/code/Magento/Directory/Test/Unit/Model/Currency/Import/FixerIoTest.php @@ -74,7 +74,7 @@ public function testFetchRates(): void $responseBody = '{"success":"true","base":"USD","date":"2015-10-07","rates":{"EUR":0.9022}}'; $expectedCurrencyRateList = ['USD' => ['EUR' => 0.9022, 'UAH' => null]]; $message = "We can't retrieve a rate from " - . "http://data.fixer.io/api/latest?access_key=api_key&base=USD&symbols=EUR,UAH for UAH."; + . "http://data.fixer.io for UAH."; $this->scopeConfig->method('getValue') ->withConsecutive( diff --git a/app/code/Magento/Directory/etc/adminhtml/system.xml b/app/code/Magento/Directory/etc/adminhtml/system.xml index 7d650b14b3d97..5f97a5e8d90d6 100644 --- a/app/code/Magento/Directory/etc/adminhtml/system.xml +++ b/app/code/Magento/Directory/etc/adminhtml/system.xml @@ -48,7 +48,7 @@ </group> <group id="currencyconverterapi" translate="label" sortOrder="45" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Currency Converter API</label> - <field id="api_key" translate="label" type="obscure" sortOrder="0" showInDefault="1" showInWebsite="0" showInStore="0"> + <field id="api_key" translate="label" type="obscure" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0"> <label>API Key</label> <config_path>currency/currencyconverterapi/api_key</config_path> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> @@ -67,27 +67,45 @@ <field id="error_email" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Error Email Recipient</label> <validate>validate-email</validate> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="error_email_identity" translate="label" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Error Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="error_email_template" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Error Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="frequency" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Frequency</label> <source_model>Magento\Cron\Model\Config\Source\Frequency</source_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="service" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Service</label> <source_model>Magento\Directory\Model\Currency\Import\Source\Service</source_model> <backend_model>Magento\Config\Model\Config\Backend\Currency\Cron</backend_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="time" translate="label" type="time" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Start Time</label> + <depends> + <field id="enabled">1</field> + </depends> </field> </group> </section> diff --git a/app/code/Magento/Directory/etc/config.xml b/app/code/Magento/Directory/etc/config.xml index c18c4f29d5822..2ff0b484fe979 100644 --- a/app/code/Magento/Directory/etc/config.xml +++ b/app/code/Magento/Directory/etc/config.xml @@ -36,7 +36,7 @@ <general> <country> <optional_zip_countries>HK,IE,MO,PA,GB</optional_zip_countries> - <allow>AF,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AX,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BL,BT,BO,BA,BW,BV,BR,IO,VG,BN,BG,BF,BI,KH,CM,CA,CD,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CK,CR,HR,CU,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GG,GH,GI,GR,GL,GD,GP,GU,GT,GN,GW,GY,HT,HM,HN,HK,HU,IS,IM,IN,ID,IR,IQ,IE,IL,IT,CI,JE,JM,JP,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,ME,MF,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,FX,MX,FM,MD,MC,MN,MS,MA,MZ,MM,NA,NR,NP,NL,AN,NC,NZ,NI,NE,NG,NU,NF,KP,MP,NO,OM,PK,PW,PA,PG,PY,PE,PH,PN,PL,PS,PT,PR,QA,RE,RO,RS,RU,RW,SH,KN,LC,PM,VC,WS,SM,ST,SA,SN,SC,SL,SG,SK,SI,SB,SO,ZA,GS,KR,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TL,TW,TJ,TZ,TH,TG,TK,TO,TT,TN,TR,TM,TC,TV,VI,UG,UA,AE,GB,US,UM,UY,UZ,VU,VA,VE,VN,WF,EH,YE,ZM,ZW</allow> + <allow>AF,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AX,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BL,BT,BO,BQ,BA,BW,BV,BR,IO,VG,BN,BG,BF,BI,KH,CM,CA,CD,CV,KY,CF,TD,CL,CN,CX,CW,CC,CO,KM,CG,CK,CR,HR,CU,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GG,GH,GI,GR,GL,GD,GP,GU,GT,GN,GW,GY,HT,HM,HN,HK,HU,IS,IM,IN,ID,IR,IQ,IE,IL,IT,CI,JE,JM,JP,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,ME,MF,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,FX,MX,FM,MD,MC,MN,MS,MA,MZ,MM,NA,NR,NP,NL,AN,NC,NZ,NI,NE,NG,NU,NF,KP,MP,NO,OM,PK,PW,PA,PG,PY,PE,PH,PN,PL,PS,PT,PR,QA,RE,RO,RS,RU,RW,SH,KN,LC,PM,VC,WS,SM,ST,SA,SN,SC,SL,SG,SK,SI,SB,SO,ZA,GS,KR,ES,LK,SD,SR,SJ,SZ,SE,CH,SX,SY,TL,TW,TJ,TZ,TH,TG,TK,TO,TT,TN,TR,TM,TC,TV,VI,UG,UA,AE,GB,US,UM,UY,UZ,VU,VA,VE,VN,WF,EH,XK,YE,ZM,ZW</allow> <default>US</default> </country> <locale> diff --git a/app/code/Magento/Directory/etc/crontab.xml b/app/code/Magento/Directory/etc/crontab.xml index d6868ff6aa0d6..589cd394d7cf1 100644 --- a/app/code/Magento/Directory/etc/crontab.xml +++ b/app/code/Magento/Directory/etc/crontab.xml @@ -7,6 +7,8 @@ --> <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="currency_rates_update" instance="Magento\Directory\Model\Observer" method="scheduledUpdateCurrencyRates" /> + <job name="currency_rates_update" instance="Magento\Directory\Model\Observer" method="scheduledUpdateCurrencyRates"> + <config_path>crontab/default/jobs/currency_rates_update/schedule/cron_expr</config_path> + </job> </group> </config> diff --git a/app/code/Magento/Directory/etc/db_schema.xml b/app/code/Magento/Directory/etc/db_schema.xml index c11e0ee525e37..163e972423b98 100644 --- a/app/code/Magento/Directory/etc/db_schema.xml +++ b/app/code/Magento/Directory/etc/db_schema.xml @@ -8,7 +8,7 @@ <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="directory_country" resource="default" engine="innodb" comment="Directory Country"> - <column xsi:type="varchar" name="country_id" nullable="false" length="2" comment="Country Id in ISO-2"/> + <column xsi:type="varchar" name="country_id" nullable="false" length="2" comment="Country ID in ISO-2"/> <column xsi:type="varchar" name="iso2_code" nullable="true" length="2" comment="Country ISO-2 format"/> <column xsi:type="varchar" name="iso3_code" nullable="true" length="3" comment="Country ISO-3"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -17,8 +17,8 @@ </table> <table name="directory_country_format" resource="default" engine="innodb" comment="Directory Country Format"> <column xsi:type="int" name="country_format_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Country Format Id"/> - <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country Id in ISO-2"/> + comment="Country Format ID"/> + <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country ID in ISO-2"/> <column xsi:type="varchar" name="type" nullable="true" length="30" comment="Country Format Type"/> <column xsi:type="text" name="format" nullable="false" comment="Country Format"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -31,9 +31,9 @@ </table> <table name="directory_country_region" resource="default" engine="innodb" comment="Directory Country Region"> <column xsi:type="int" name="region_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Region Id"/> + comment="Region ID"/> <column xsi:type="varchar" name="country_id" nullable="false" length="4" default="0" - comment="Country Id in ISO-2"/> + comment="Country ID in ISO-2"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Region code"/> <column xsi:type="varchar" name="default_name" nullable="true" length="255" comment="Region Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -47,7 +47,7 @@ comment="Directory Country Region Name"> <column xsi:type="varchar" name="locale" nullable="false" length="8" comment="Locale"/> <column xsi:type="int" name="region_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Region Id"/> + default="0" comment="Region ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Region Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="locale"/> diff --git a/app/code/Magento/Directory/etc/di.xml b/app/code/Magento/Directory/etc/di.xml index 50cd65cc5045c..fb2c526ac730b 100644 --- a/app/code/Magento/Directory/etc/di.xml +++ b/app/code/Magento/Directory/etc/di.xml @@ -35,6 +35,7 @@ <item name="DE" xsi:type="string">DE</item> <item name="AT" xsi:type="string">AT</item> <item name="FI" xsi:type="string">FI</item> + <item name="BE" xsi:type="string">BE</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Downloadable/Api/DomainManagerInterface.php b/app/code/Magento/Downloadable/Api/DomainManagerInterface.php new file mode 100644 index 0000000000000..ca98f18e36c33 --- /dev/null +++ b/app/code/Magento/Downloadable/Api/DomainManagerInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Downloadable\Api; + +/** + * Interface DomainManagerInterface + * Manage downloadable domains whitelist. + */ +interface DomainManagerInterface +{ + /** + * Get the whitelist. + * + * @return array + */ + public function getDomains(): array; + + /** + * Add host to the whitelist. + * + * @param array $hosts + * @return void + */ + public function addDomains(array $hosts): void; + + /** + * Remove host from the whitelist. + * + * @param array $hosts + * @return void + */ + public function removeDomains(array $hosts): void; +} diff --git a/app/code/Magento/Downloadable/Block/Checkout/Cart/Item/Renderer.php b/app/code/Magento/Downloadable/Block/Checkout/Cart/Item/Renderer.php index 51efc74738043..8b8a9b6bf2895 100644 --- a/app/code/Magento/Downloadable/Block/Checkout/Cart/Item/Renderer.php +++ b/app/code/Magento/Downloadable/Block/Checkout/Cart/Item/Renderer.php @@ -37,7 +37,7 @@ class Renderer extends \Magento\Checkout\Block\Cart\Item\Renderer * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param PriceCurrencyInterface $priceCurrency - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param InterpretationStrategyInterface $messageInterpretationStrategy * @param \Magento\Downloadable\Helper\Catalog\Product\Configuration $downloadableProductConfiguration * @param array $data @@ -51,7 +51,7 @@ public function __construct( \Magento\Framework\Url\Helper\Data $urlHelper, \Magento\Framework\Message\ManagerInterface $messageManager, PriceCurrencyInterface $priceCurrency, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, InterpretationStrategyInterface $messageInterpretationStrategy, \Magento\Downloadable\Helper\Catalog\Product\Configuration $downloadableProductConfiguration, array $data = [] diff --git a/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php b/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php new file mode 100644 index 0000000000000..76d9a13f70f1f --- /dev/null +++ b/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php @@ -0,0 +1,91 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Downloadable\Console\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Magento\Downloadable\Api\DomainManagerInterface as DomainManager; + +/** + * Class DomainsAddCommand + * + * Command for adding downloadable domain to the whitelist + */ +class DomainsAddCommand extends Command +{ + /** + * Name of domains input argument + */ + public const INPUT_KEY_DOMAINS = 'domains'; + + /** + * @var DomainManager + */ + private $domainManager; + + /** + * DomainsAddCommand constructor. + * @param DomainManager $domainManager + */ + public function __construct( + DomainManager $domainManager + ) { + $this->domainManager = $domainManager; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $description = 'Add domains to the downloadable domains whitelist'; + + $this->setName('downloadable:domains:add') + ->setDescription($description) + ->setDefinition( + [ + new InputArgument( + self::INPUT_KEY_DOMAINS, + InputArgument::IS_ARRAY, + 'Domains name' + ) + ] + ); + parent::configure(); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + try { + if ($input->getArgument(self::INPUT_KEY_DOMAINS)) { + $whitelistBefore = $this->domainManager->getDomains(); + $newDomains = $input->getArgument(self::INPUT_KEY_DOMAINS); + $newDomains = array_filter(array_map('trim', $newDomains), 'strlen'); + + $this->domainManager->addDomains($newDomains); + + foreach (array_diff($this->domainManager->getDomains(), $whitelistBefore) as $newHost) { + $output->writeln( + $newHost . ' was added to the whitelist.' + ); + } + } + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $output->writeln($e->getTraceAsString()); + } + return; + } + } +} diff --git a/app/code/Magento/Downloadable/Console/Command/DomainsRemoveCommand.php b/app/code/Magento/Downloadable/Console/Command/DomainsRemoveCommand.php new file mode 100644 index 0000000000000..a30e99a24859c --- /dev/null +++ b/app/code/Magento/Downloadable/Console/Command/DomainsRemoveCommand.php @@ -0,0 +1,91 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Downloadable\Console\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Magento\Downloadable\Api\DomainManagerInterface as DomainManager; + +/** + * Class DomainsRemoveCommand + * + * Command for removing downloadable domain from the whitelist + */ +class DomainsRemoveCommand extends Command +{ + /** + * Name of domains input argument + */ + public const INPUT_KEY_DOMAINS = 'domains'; + + /** + * @var DomainManager + */ + private $domainManager; + + /** + * DomainsRemoveCommand constructor. + * + * @param DomainManager $domainManager + */ + public function __construct( + DomainManager $domainManager + ) { + $this->domainManager = $domainManager; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $description = 'Remove domains from the downloadable domains whitelist'; + + $this->setName('downloadable:domains:remove') + ->setDescription($description) + ->setDefinition( + [ + new InputArgument( + self::INPUT_KEY_DOMAINS, + InputArgument::IS_ARRAY, + 'Domain names' + ) + ] + ); + parent::configure(); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + try { + if ($input->getArgument(self::INPUT_KEY_DOMAINS)) { + $whitelistBefore = $this->domainManager->getDomains(); + $removeDomains = $input->getArgument(self::INPUT_KEY_DOMAINS); + $removeDomains = array_filter(array_map('trim', $removeDomains), 'strlen'); + $this->domainManager->removeDomains($removeDomains); + + foreach (array_diff($whitelistBefore, $this->domainManager->getDomains()) as $removedHost) { + $output->writeln( + $removedHost . ' was removed from the whitelist.' + ); + } + } + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $output->writeln($e->getTraceAsString()); + } + return; + } + } +} diff --git a/app/code/Magento/Downloadable/Console/Command/DomainsShowCommand.php b/app/code/Magento/Downloadable/Console/Command/DomainsShowCommand.php new file mode 100644 index 0000000000000..eb4488353a096 --- /dev/null +++ b/app/code/Magento/Downloadable/Console/Command/DomainsShowCommand.php @@ -0,0 +1,67 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Downloadable\Console\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputInterface; +use Magento\Downloadable\Api\DomainManagerInterface as DomainManager; + +/** + * Class DomainsShowCommand + * + * Command for listing allowed downloadable domains + */ +class DomainsShowCommand extends Command +{ + /** + * @var DomainManager + */ + private $domainManager; + + /** + * DomainsShowCommand constructor. + * @param DomainManager $domainManager + */ + public function __construct( + DomainManager $domainManager + ) { + $this->domainManager = $domainManager; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $description = 'Display downloadable domains whitelist'; + + $this->setName('downloadable:domains:show') + ->setDescription($description); + parent::configure(); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + try { + $whitelist = implode("\n", $this->domainManager->getDomains()); + $output->writeln( + "Downloadable domains whitelist:\n$whitelist" + ); + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $output->writeln($e->getTraceAsString()); + } + return; + } + } +} diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php index 83b2797050db9..a7c32eed8bb15 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php @@ -7,6 +7,8 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; /** * Class Upload @@ -76,23 +78,27 @@ public function __construct( */ public function execute() { - $type = $this->getRequest()->getParam('type'); - $tmpPath = ''; - if ($type == 'samples') { - $tmpPath = $this->_sample->getBaseTmpPath(); - } elseif ($type == 'links') { - $tmpPath = $this->_link->getBaseTmpPath(); - } elseif ($type == 'link_samples') { - $tmpPath = $this->_link->getBaseSampleTmpPath(); - } - try { + $type = $this->getRequest()->getParam('type'); + $tmpPath = ''; + if ($type === 'samples') { + $tmpPath = $this->_sample->getBaseTmpPath(); + } elseif ($type === 'links') { + $tmpPath = $this->_link->getBaseTmpPath(); + } elseif ($type === 'link_samples') { + $tmpPath = $this->_link->getBaseSampleTmpPath(); + } else { + throw new LocalizedException(__('Upload type can not be determined.')); + } + $uploader = $this->uploaderFactory->create(['fileId' => $type]); $result = $this->_fileHelper->uploadFromTmp($tmpPath, $uploader); if (!$result) { - throw new \Exception('File can not be moved from temporary folder to the destination folder.'); + throw new FileSystemException( + __('File can not be moved from temporary folder to the destination folder.') + ); } unset($result['tmp_name'], $result['path']); @@ -101,7 +107,7 @@ public function execute() $relativePath = rtrim($tmpPath, '/') . '/' . ltrim($result['file'], '/'); $this->storageDatabase->saveFile($relativePath); } - } catch (\Exception $e) { + } catch (\Throwable $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php index a283891afc406..f310b376633d3 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php @@ -5,11 +5,14 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Product\Initialization\Helper\Plugin; -use Magento\Framework\App\RequestInterface; +use Magento\Downloadable\Api\Data\LinkInterfaceFactory; +use Magento\Downloadable\Api\Data\SampleInterfaceFactory; +use Magento\Downloadable\Helper\Download; use Magento\Downloadable\Model\Link\Builder as LinkBuilder; +use Magento\Downloadable\Model\Product\Type; +use Magento\Downloadable\Model\ResourceModel\Sample\Collection; use Magento\Downloadable\Model\Sample\Builder as SampleBuilder; -use Magento\Downloadable\Api\Data\SampleInterfaceFactory; -use Magento\Downloadable\Api\Data\LinkInterfaceFactory; +use Magento\Framework\App\RequestInterface; /** * Class for initialization downloadable info from request. @@ -42,8 +45,6 @@ class Downloadable private $linkBuilder; /** - * Constructor - * * @param RequestInterface $request * @param LinkBuilder $linkBuilder * @param SampleBuilder $sampleBuilder @@ -79,14 +80,18 @@ public function afterInitialize( \Magento\Catalog\Model\Product $product ) { if ($downloadable = $this->request->getPost('downloadable')) { + $product->setTypeId(Type::TYPE_DOWNLOADABLE); $product->setDownloadableData($downloadable); $extension = $product->getExtensionAttributes(); + $productLinks = $product->getTypeInstance()->getLinks($product); + $productSamples = $product->getTypeInstance()->getSamples($product); if (isset($downloadable['link']) && is_array($downloadable['link'])) { $links = []; foreach ($downloadable['link'] as $linkData) { if (!$linkData || (isset($linkData['is_delete']) && $linkData['is_delete'])) { continue; } else { + $linkData = $this->processLink($linkData, $productLinks); $links[] = $this->linkBuilder->setData( $linkData )->build( @@ -104,6 +109,7 @@ public function afterInitialize( if (!$sampleData || (isset($sampleData['is_delete']) && (bool)$sampleData['is_delete'])) { continue; } else { + $sampleData = $this->processSample($sampleData, $productSamples); $samples[] = $this->sampleBuilder->setData( $sampleData )->build( @@ -124,4 +130,69 @@ public function afterInitialize( } return $product; } + + /** + * Check Links type and status. + * + * @param array $linkData + * @param array $productLinks + * @return array + */ + private function processLink(array $linkData, array $productLinks): array + { + $linkId = $linkData['link_id'] ?? null; + if ($linkId && isset($productLinks[$linkId])) { + $linkData = $this->processFileStatus($linkData, $productLinks[$linkId]->getLinkFile()); + $linkData['sample'] = $this->processFileStatus( + $linkData['sample'] ?? [], + $productLinks[$linkId]->getSampleFile() + ); + } else { + $linkData = $this->processFileStatus($linkData, null); + $linkData['sample'] = $this->processFileStatus($linkData['sample'] ?? [], null); + } + + return $linkData; + } + + /** + * Check Sample type and status. + * + * @param array $sampleData + * @param Collection $productSamples + * @return array + */ + private function processSample(array $sampleData, Collection $productSamples): array + { + $sampleId = $sampleData['sample_id'] ?? null; + /** @var \Magento\Downloadable\Model\Sample $productSample */ + $productSample = $sampleId ? $productSamples->getItemById($sampleId) : null; + if ($sampleId && $productSample) { + $sampleData = $this->processFileStatus($sampleData, $productSample->getSampleFile()); + } else { + $sampleData = $this->processFileStatus($sampleData, null); + } + + return $sampleData; + } + + /** + * Compare file path from request with DB and set status. + * + * @param array $data + * @param string|null $file + * @return array + */ + private function processFileStatus(array $data, ?string $file): array + { + if (isset($data['type']) && $data['type'] === Download::LINK_TYPE_FILE && isset($data['file']['0']['file'])) { + if ($data['file'][0]['file'] !== $file) { + $data['file'][0]['status'] = 'new'; + } else { + $data['file'][0]['status'] = 'old'; + } + } + + return $data; + } } diff --git a/app/code/Magento/Downloadable/Model/DomainManager.php b/app/code/Magento/Downloadable/Model/DomainManager.php new file mode 100644 index 0000000000000..e1f275902b034 --- /dev/null +++ b/app/code/Magento/Downloadable/Model/DomainManager.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Downloadable\Model; + +use Magento\Framework\App\DeploymentConfig\Writer as ConfigWriter; +use Magento\Downloadable\Api\DomainManagerInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\File\ConfigFilePool; + +/** + * Class DomainManager + * + * Manage downloadable domains whitelist in the environment config. + */ +class DomainManager implements DomainManagerInterface +{ + /** + * Path to the allowed domains in the deployment config + */ + private const PARAM_DOWNLOADABLE_DOMAINS = 'downloadable_domains'; + + /** + * @var ConfigWriter + */ + private $configWriter; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * DomainManager constructor. + * + * @param ConfigWriter $configWriter + * @param DeploymentConfig $deploymentConfig + */ + public function __construct( + ConfigWriter $configWriter, + DeploymentConfig $deploymentConfig + ) { + $this->configWriter = $configWriter; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * @inheritdoc + */ + public function getDomains(): array + { + return array_map('strtolower', $this->deploymentConfig->get(self::PARAM_DOWNLOADABLE_DOMAINS) ?? []); + } + + /** + * @inheritdoc + */ + public function addDomains(array $hosts): void + { + $whitelist = $this->getDomains(); + foreach (array_map('strtolower', $hosts) as $host) { + if (!in_array($host, $whitelist)) { + $whitelist[] = $host; + } + } + + $this->configWriter->saveConfig( + [ + ConfigFilePool::APP_ENV => [ + self::PARAM_DOWNLOADABLE_DOMAINS => $whitelist + ] + ], + true + ); + } + + /** + * @inheritdoc + */ + public function removeDomains(array $hosts): void + { + $whitelist = $this->getDomains(); + foreach (array_map('strtolower', $hosts) as $host) { + if (in_array($host, $whitelist)) { + $index = array_search($host, $whitelist); + unset($whitelist[$index]); + } + } + + $whitelist = array_values($whitelist); // reindex whitelist to prevent non-sequential keying + + $this->configWriter->saveConfig( + [ + ConfigFilePool::APP_ENV => [ + self::PARAM_DOWNLOADABLE_DOMAINS => $whitelist + ] + ], + true + ); + } +} diff --git a/app/code/Magento/Downloadable/Model/Link/Builder.php b/app/code/Magento/Downloadable/Model/Link/Builder.php index 83d01f76fe9cd..ff76f7eeda440 100644 --- a/app/code/Magento/Downloadable/Model/Link/Builder.php +++ b/app/code/Magento/Downloadable/Model/Link/Builder.php @@ -69,6 +69,8 @@ public function __construct( } /** + * Set Data. + * * @param array $data * @return $this * @since 100.1.0 @@ -80,6 +82,8 @@ public function setData(array $data) } /** + * Build correct data structure. + * * @param \Magento\Downloadable\Api\Data\LinkInterface $link * @return \Magento\Downloadable\Api\Data\LinkInterface * @throws \Magento\Framework\Exception\LocalizedException @@ -134,6 +138,8 @@ public function build(\Magento\Downloadable\Api\Data\LinkInterface $link) } /** + * Reset data. + * * @return void */ private function resetData() @@ -142,6 +148,8 @@ private function resetData() } /** + * Get existing component or create new. + * * @return Link */ private function getComponent() @@ -153,6 +161,8 @@ private function getComponent() } /** + * Build correct sample structure. + * * @param \Magento\Downloadable\Api\Data\LinkInterface $link * @param array $sample * @return \Magento\Downloadable\Api\Data\LinkInterface @@ -174,7 +184,8 @@ private function buildSample(\Magento\Downloadable\Api\Data\LinkInterface $link, ), \Magento\Downloadable\Api\Data\LinkInterface::class ); - if ($link->getSampleType() === \Magento\Downloadable\Helper\Download::LINK_TYPE_FILE) { + if ($link->getSampleType() === \Magento\Downloadable\Helper\Download::LINK_TYPE_FILE + && isset($sample['file'])) { $linkSampleFileName = $this->downloadableFile->moveFileFromTmp( $this->getComponent()->getBaseSampleTmpPath(), $this->getComponent()->getBaseSamplePath(), diff --git a/app/code/Magento/Downloadable/Model/Link/ContentValidator.php b/app/code/Magento/Downloadable/Model/Link/ContentValidator.php index 088356caefad0..28d61986a4f56 100644 --- a/app/code/Magento/Downloadable/Model/Link/ContentValidator.php +++ b/app/code/Magento/Downloadable/Model/Link/ContentValidator.php @@ -6,12 +6,29 @@ namespace Magento\Downloadable\Model\Link; use Magento\Downloadable\Api\Data\LinkInterface; +use Magento\Downloadable\Helper\File; use Magento\Downloadable\Model\File\ContentValidator as FileContentValidator; use Magento\Framework\Exception\InputException; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Url\Validator as UrlValidator; +use Magento\Downloadable\Model\Url\DomainValidator; +/** + * Class to validate Link Content. + */ class ContentValidator { + /** + * @var DomainValidator + */ + private $domainValidator; + + /** + * @var File + */ + private $fileHelper; + /** * @var FileContentValidator */ @@ -25,17 +42,23 @@ class ContentValidator /** * @param FileContentValidator $fileContentValidator * @param UrlValidator $urlValidator + * @param DomainValidator $domainValidator + * @param File|null $fileHelper */ public function __construct( FileContentValidator $fileContentValidator, - UrlValidator $urlValidator + UrlValidator $urlValidator, + DomainValidator $domainValidator, + File $fileHelper = null ) { $this->fileContentValidator = $fileContentValidator; $this->urlValidator = $urlValidator; + $this->domainValidator = $domainValidator; + $this->fileHelper = $fileHelper ?? ObjectManager::getInstance()->get(File::class); } /** - * Check if link content is valid + * Check if link content is valid. * * @param LinkInterface $link * @param bool $validateLinkContent @@ -63,50 +86,74 @@ public function isValid(LinkInterface $link, $validateLinkContent = true, $valid if ($validateSampleContent) { $this->validateSampleResource($link); } + return true; } /** - * Validate link resource (file or URL) + * Validate link resource (file or URL). * * @param LinkInterface $link - * @throws InputException * @return void + * @throws InputException */ protected function validateLinkResource(LinkInterface $link) { - if ($link->getLinkType() == 'url' - && !$this->urlValidator->isValid($link->getLinkUrl()) - ) { - throw new InputException(__('Link URL must have valid format.')); - } - if ($link->getLinkType() == 'file' - && (!$link->getLinkFileContent() - || !$this->fileContentValidator->isValid($link->getLinkFileContent())) - ) { - throw new InputException(__('Provided file content must be valid base64 encoded data.')); + if ($link->getLinkType() === 'url') { + if (!$this->urlValidator->isValid($link->getLinkUrl())) { + throw new InputException(__('Link URL must have valid format.')); + } + + if (!$this->domainValidator->isValid($link->getLinkUrl())) { + throw new InputException(__('Link URL\'s domain is not in list of downloadable_domains in env.php.')); + } + } elseif ($link->getLinkFileContent()) { + if (!$this->fileContentValidator->isValid($link->getLinkFileContent())) { + throw new InputException(__('Provided file content must be valid base64 encoded data.')); + } + } elseif (!$this->isFileValid($link->getBasePath() . $link->getLinkFile())) { + throw new InputException(__('Link file not found. Please try again.')); } } /** - * Validate sample resource (file or URL) + * Validate sample resource (file or URL). * * @param LinkInterface $link - * @throws InputException * @return void + * @throws InputException */ protected function validateSampleResource(LinkInterface $link) { - if ($link->getSampleType() == 'url' - && !$this->urlValidator->isValid($link->getSampleUrl()) - ) { - throw new InputException(__('Sample URL must have valid format.')); + if ($link->getSampleType() === 'url') { + if (!$this->urlValidator->isValid($link->getSampleUrl())) { + throw new InputException(__('Sample URL must have valid format.')); + } + + if (!$this->domainValidator->isValid($link->getSampleUrl())) { + throw new InputException(__('Sample URL\'s domain is not in list of downloadable_domains in env.php.')); + } + } elseif ($link->getSampleFileContent()) { + if (!$this->fileContentValidator->isValid($link->getSampleFileContent())) { + throw new InputException(__('Provided file content must be valid base64 encoded data.')); + } + } elseif (!$this->isFileValid($link->getBaseSamplePath() . $link->getSampleFile())) { + throw new InputException(__('Link sample file not found. Please try again.')); } - if ($link->getSampleType() == 'file' - && (!$link->getSampleFileContent() - || !$this->fileContentValidator->isValid($link->getSampleFileContent())) - ) { - throw new InputException(__('Provided file content must be valid base64 encoded data.')); + } + + /** + * Check that Links File or Sample is valid. + * + * @param string $file + * @return bool + */ + private function isFileValid(string $file): bool + { + try { + return $this->fileHelper->ensureFileInFilesystem($file); + } catch (ValidatorException $e) { + return false; } } } diff --git a/app/code/Magento/Downloadable/Model/LinkRepository.php b/app/code/Magento/Downloadable/Model/LinkRepository.php index 0898f1924e538..68fbba2a7d385 100644 --- a/app/code/Magento/Downloadable/Model/LinkRepository.php +++ b/app/code/Magento/Downloadable/Model/LinkRepository.php @@ -97,7 +97,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -107,6 +107,8 @@ public function getList($sku) } /** + * @inheritdoc + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @return array */ @@ -166,9 +168,11 @@ protected function setBasicFields($resourceData, $dataObject) } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws InputException */ public function save($sku, LinkInterface $link, $isGlobalScopeContent = true) { @@ -176,29 +180,28 @@ public function save($sku, LinkInterface $link, $isGlobalScopeContent = true) if ($link->getId() !== null) { return $this->updateLink($product, $link, $isGlobalScopeContent); } else { - if ($product->getTypeId() !== \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { + if ($product->getTypeId() !== Type::TYPE_DOWNLOADABLE) { throw new InputException( __('The product needs to be the downloadable type. Verify the product and try again.') ); } - $validateLinkContent = !($link->getLinkType() === 'file' && $link->getLinkFile()); - $validateSampleContent = !($link->getSampleType() === 'file' && $link->getSampleFile()); - if (!$this->contentValidator->isValid($link, $validateLinkContent, $validateSampleContent)) { + $this->validateLinkType($link); + $this->validateSampleType($link); + if (!$this->contentValidator->isValid($link, true, $link->hasSampleType())) { throw new InputException(__('The link information is invalid. Verify the link and try again.')); } - - if (!in_array($link->getLinkType(), ['url', 'file'], true)) { - throw new InputException(__('The link type is invalid. Verify and try again.')); - } $title = $link->getTitle(); if (empty($title)) { throw new InputException(__('The link title is empty. Enter the link title and try again.')); } + return $this->saveLink($product, $link, $isGlobalScopeContent); } } /** + * Construct Data structure and Save it. + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param LinkInterface $link * @param bool $isGlobalScopeContent @@ -220,7 +223,7 @@ protected function saveLink( 'is_shareable' => $link->getIsShareable(), ]; - if ($link->getLinkType() == 'file' && $link->getLinkFile() === null) { + if ($link->getLinkType() == 'file' && $link->getLinkFileContent() !== null) { $linkData['file'] = $this->jsonEncoder->encode( [ $this->fileContentUploader->upload($link->getLinkFileContent(), 'link_file'), @@ -242,7 +245,7 @@ protected function saveLink( if ($link->getSampleType() == 'file') { $linkData['sample']['type'] = 'file'; - if ($link->getSampleFile() === null) { + if ($link->getSampleFileContent() !== null) { $fileData = [ $this->fileContentUploader->upload($link->getSampleFileContent(), 'link_sample_file'), ]; @@ -269,6 +272,8 @@ protected function saveLink( } /** + * Update existing Link. + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param LinkInterface $link * @param bool $isGlobalScopeContent @@ -298,9 +303,10 @@ protected function updateLink( __("The downloadable link isn't related to the product. Verify the link and try again.") ); } - $validateLinkContent = !($link->getLinkFileContent() === null); - $validateSampleContent = !($link->getSampleFileContent() === null); - if (!$this->contentValidator->isValid($link, $validateLinkContent, $validateSampleContent)) { + $this->validateLinkType($link); + $this->validateSampleType($link); + $validateSampleContent = $link->hasSampleType(); + if (!$this->contentValidator->isValid($link, true, $validateSampleContent)) { throw new InputException(__('The link information is invalid. Verify the link and try again.')); } if ($isGlobalScopeContent) { @@ -312,20 +318,16 @@ protected function updateLink( throw new InputException(__('The link title is empty. Enter the link title and try again.')); } } - - if ($link->getLinkType() == 'file' && $link->getLinkFileContent() === null && !$link->getLinkFile()) { - $link->setLinkFile($existingLink->getLinkFile()); - } - if ($link->getSampleType() == 'file' && $link->getSampleFileContent() === null && !$link->getSampleFile()) { - $link->setSampleFile($existingLink->getSampleFile()); + if (!$validateSampleContent) { + $this->resetLinkSampleContent($link, $existingLink); } - $this->saveLink($product, $link, $isGlobalScopeContent); + return $existingLink->getId(); } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($id) { @@ -344,6 +346,52 @@ public function delete($id) return true; } + /** + * Check that Link type exist. + * + * @param LinkInterface $link + * @return void + * @throws InputException + */ + private function validateLinkType(LinkInterface $link): void + { + if (!in_array($link->getLinkType(), ['url', 'file'], true)) { + throw new InputException(__('The link type is invalid. Verify and try again.')); + } + } + + /** + * Check that Link sample type exist. + * + * @param LinkInterface $link + * @return void + * @throws InputException + */ + private function validateSampleType(LinkInterface $link): void + { + if ($link->hasSampleType() && !in_array($link->getSampleType(), ['url', 'file'], true)) { + throw new InputException(__('The link sample type is invalid. Verify and try again.')); + } + } + + /** + * Reset Sample type and file. + * + * @param LinkInterface $link + * @param LinkInterface $existingLink + * @return void + */ + private function resetLinkSampleContent(LinkInterface $link, LinkInterface $existingLink): void + { + $existingType = $existingLink->getSampleType(); + $link->setSampleType($existingType); + if ($existingType === 'file') { + $link->setSampleFile($existingLink->getSampleFile()); + } else { + $link->setSampleUrl($existingLink->getSampleUrl()); + } + } + /** * Get MetadataPool instance * diff --git a/app/code/Magento/Downloadable/Model/Sample/ContentValidator.php b/app/code/Magento/Downloadable/Model/Sample/ContentValidator.php index 6a273bfe5d34e..697878017ee8b 100644 --- a/app/code/Magento/Downloadable/Model/Sample/ContentValidator.php +++ b/app/code/Magento/Downloadable/Model/Sample/ContentValidator.php @@ -6,12 +6,29 @@ namespace Magento\Downloadable\Model\Sample; use Magento\Downloadable\Api\Data\SampleInterface; +use Magento\Downloadable\Helper\File; use Magento\Downloadable\Model\File\ContentValidator as FileContentValidator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Url\Validator as UrlValidator; +use Magento\Downloadable\Model\Url\DomainValidator; +/** + * Class to validate Sample Content. + */ class ContentValidator { + /** + * @var File + */ + private $fileHelper; + + /** + * @var DomainValidator + */ + private $domainValidator; + /** * @var UrlValidator */ @@ -25,17 +42,23 @@ class ContentValidator /** * @param FileContentValidator $fileContentValidator * @param UrlValidator $urlValidator + * @param DomainValidator $domainValidator + * @param File|null $fileHelper */ public function __construct( FileContentValidator $fileContentValidator, - UrlValidator $urlValidator + UrlValidator $urlValidator, + DomainValidator $domainValidator, + File $fileHelper = null ) { $this->fileContentValidator = $fileContentValidator; $this->urlValidator = $urlValidator; + $this->domainValidator = $domainValidator; + $this->fileHelper = $fileHelper ?? ObjectManager::getInstance()->get(File::class); } /** - * Check if sample content is valid + * Check if sample content is valid. * * @param SampleInterface $sample * @param bool $validateSampleContent @@ -51,29 +74,48 @@ public function isValid(SampleInterface $sample, $validateSampleContent = true) if ($validateSampleContent) { $this->validateSampleResource($sample); } + return true; } /** - * Validate sample resource (file or URL) + * Validate sample resource (file or URL). * * @param SampleInterface $sample - * @throws InputException * @return void + * @throws InputException */ protected function validateSampleResource(SampleInterface $sample) { - $sampleFile = $sample->getSampleFileContent(); - if ($sample->getSampleType() == 'file' - && (!$sampleFile || !$this->fileContentValidator->isValid($sampleFile)) - ) { - throw new InputException(__('Provided file content must be valid base64 encoded data.')); + if ($sample->getSampleType() === 'url') { + if (!$this->urlValidator->isValid($sample->getSampleUrl())) { + throw new InputException(__('Sample URL must have valid format.')); + } + + if (!$this->domainValidator->isValid($sample->getSampleUrl())) { + throw new InputException(__('Sample URL\'s domain is not in list of downloadable_domains in env.php.')); + } + } elseif ($sample->getSampleFileContent()) { + if (!$this->fileContentValidator->isValid($sample->getSampleFileContent())) { + throw new InputException(__('Provided file content must be valid base64 encoded data.')); + } + } elseif (!$this->isFileValid($sample->getBasePath() . $sample->getSampleFile())) { + throw new InputException(__('Sample file not found. Please try again.')); } + } - if ($sample->getSampleType() == 'url' - && !$this->urlValidator->isValid($sample->getSampleUrl()) - ) { - throw new InputException(__('Sample URL must have valid format.')); + /** + * Check that Samples file is valid. + * + * @param string $file + * @return bool + */ + private function isFileValid(string $file): bool + { + try { + return $this->fileHelper->ensureFileInFilesystem($file); + } catch (ValidatorException $e) { + return false; } } } diff --git a/app/code/Magento/Downloadable/Model/SampleRepository.php b/app/code/Magento/Downloadable/Model/SampleRepository.php index 07c7631fade13..37f376e666243 100644 --- a/app/code/Magento/Downloadable/Model/SampleRepository.php +++ b/app/code/Magento/Downloadable/Model/SampleRepository.php @@ -189,17 +189,12 @@ public function save( __('The product needs to be the downloadable type. Verify the product and try again.') ); } - $validateSampleContent = !($sample->getSampleType() === 'file' && $sample->getSampleFile()); - if (!$this->contentValidator->isValid($sample, $validateSampleContent)) { + $this->validateSampleType($sample); + if (!$this->contentValidator->isValid($sample, true)) { throw new InputException( __('The sample information is invalid. Verify the information and try again.') ); } - - if (!in_array($sample->getSampleType(), ['url', 'file'], true)) { - throw new InputException(__('The sample type is invalid. Verify the sample type and try again.')); - } - $title = $sample->getTitle(); if (empty($title)) { throw new InputException(__('The sample title is empty. Enter the title and try again.')); @@ -230,7 +225,7 @@ protected function saveSample( 'title' => $sample->getTitle(), ]; - if ($sample->getSampleType() === 'file' && $sample->getSampleFile() === null) { + if ($sample->getSampleType() === 'file' && $sample->getSampleFileContent() !== null) { $sampleData['file'] = $this->jsonEncoder->encode( [ $this->fileContentUploader->upload($sample->getSampleFileContent(), 'sample'), @@ -293,9 +288,8 @@ protected function updateSample( __("The downloadable sample isn't related to the product. Verify the link and try again.") ); } - - $validateFileContent = $sample->getSampleFileContent() === null ? false : true; - if (!$this->contentValidator->isValid($sample, $validateFileContent)) { + $this->validateSampleType($sample); + if (!$this->contentValidator->isValid($sample, true)) { throw new InputException(__('The sample information is invalid. Verify the information and try again.')); } if ($isGlobalScopeContent) { @@ -312,14 +306,8 @@ protected function updateSample( } else { $existingSample->setTitle($sample->getTitle()); } - - if ($sample->getSampleType() === 'file' - && $sample->getSampleFileContent() === null - && $sample->getSampleFile() !== null - ) { - $existingSample->setSampleFile($sample->getSampleFile()); - } $this->saveSample($product, $sample, $isGlobalScopeContent); + return $existingSample->getId(); } @@ -343,6 +331,20 @@ public function delete($id) return true; } + /** + * Check that Sample type exist. + * + * @param SampleInterface $sample + * @throws InputException + * @return void + */ + private function validateSampleType(SampleInterface $sample): void + { + if (!in_array($sample->getSampleType(), ['url', 'file'], true)) { + throw new InputException(__('The sample type is invalid. Verify the sample type and try again.')); + } + } + /** * Get MetadataPool instance * diff --git a/app/code/Magento/Downloadable/Model/Url/DomainValidator.php b/app/code/Magento/Downloadable/Model/Url/DomainValidator.php new file mode 100644 index 0000000000000..cab7fb134ea33 --- /dev/null +++ b/app/code/Magento/Downloadable/Model/Url/DomainValidator.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Downloadable\Model\Url; + +use Magento\Downloadable\Api\DomainManagerInterface as DomainManager; +use Magento\Framework\Validator\Ip as IpValidator; +use Zend\Uri\Uri as UriHandler; + +/** + * Class is responsible for checking if downloadable product link domain is allowed. + */ +class DomainValidator +{ + /** + * Path to the allowed domains in the deployment config + */ + public const PARAM_DOWNLOADABLE_DOMAINS = 'downloadable_domains'; + + /** + * @var IpValidator + */ + private $ipValidator; + + /** + * @var UriHandler + */ + private $uriHandler; + + /** + * @var DomainManager + */ + private $domainManager; + + /** + * @param DomainManager $domainManager + * @param IpValidator $ipValidator + * @param UriHandler $uriHandler + */ + public function __construct( + DomainManager $domainManager, + IpValidator $ipValidator, + UriHandler $uriHandler + ) { + $this->domainManager = $domainManager; + $this->ipValidator = $ipValidator; + $this->uriHandler = $uriHandler; + } + + /** + * Validate url input. + * + * Assert parsed host of $value is contained within environment whitelist + * + * @param string $value + * @return bool + */ + public function isValid($value): bool + { + $host = $this->getHost($value); + + $isIpAddress = $this->ipValidator->isValid($host); + $isValid = !$isIpAddress && in_array($host, $this->domainManager->getDomains()); + + return $isValid; + } + + /** + * Extract host from url + * + * @param string $url + * @return string + */ + private function getHost($url): string + { + $host = $this->uriHandler->parse($url)->getHost(); + + if ($host === null) { + return ''; + } + + // ipv6 hosts are brace-delimited in url; they are removed here for subsequent validation + return trim($host, '[] '); + } +} diff --git a/app/code/Magento/Downloadable/Setup/Patch/Data/AddDownloadableHostsConfig.php b/app/code/Magento/Downloadable/Setup/Patch/Data/AddDownloadableHostsConfig.php new file mode 100644 index 0000000000000..0e88bd166b604 --- /dev/null +++ b/app/code/Magento/Downloadable/Setup/Patch/Data/AddDownloadableHostsConfig.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Downloadable\Setup\Patch\Data; + +use Magento\Config\Model\Config\Backend\Admin\Custom; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Zend\Uri\Uri as UriHandler; +use Magento\Framework\Url\ScopeResolverInterface; +use Magento\Downloadable\Api\DomainManagerInterface as DomainManager; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Backend\App\Area\FrontNameResolver; + +/** + * Adding base url as allowed downloadable domain. + */ +class AddDownloadableHostsConfig implements DataPatchInterface +{ + /** + * @var UriHandler + */ + private $uriHandler; + + /** + * @var ScopeResolverInterface + */ + private $scopeResolver; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var DomainManager + */ + private $domainManager; + + /** + * @var array + */ + private $whitelist = []; + + /** + * AddDownloadableHostsConfig constructor. + * + * @param UriHandler $uriHandler + * @param ScopeResolverInterface $scopeResolver + * @param ScopeConfigInterface $scopeConfig + * @param DomainManager $domainManager + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + UriHandler $uriHandler, + ScopeResolverInterface $scopeResolver, + ScopeConfigInterface $scopeConfig, + DomainManager $domainManager, + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->uriHandler = $uriHandler; + $this->scopeResolver = $scopeResolver; + $this->scopeConfig = $scopeConfig; + $this->domainManager = $domainManager; + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $customStoreScope = $this->scopeResolver->getScope(Custom::CONFIG_SCOPE_ID); + $storeScopes = $this->scopeResolver->getScopes(); + $allStoreScopes = array_merge($storeScopes, [$customStoreScope]); + + foreach ($allStoreScopes as $scope) { + $this->addStoreAndWebsiteUrlsFromScope($scope); + } + + $customAdminUrl = $this->scopeConfig->getValue( + FrontNameResolver::XML_PATH_CUSTOM_ADMIN_URL, + ScopeInterface::SCOPE_STORE + ); + + if ($customAdminUrl) { + $this->addHost($customAdminUrl); + } + + if ($this->moduleDataSetup->tableExists('downloadable_link')) { + $select = $this->moduleDataSetup->getConnection() + ->select() + ->from( + $this->moduleDataSetup->getTable('downloadable_link'), + ['link_url'] + ) + ->where('link_type = ?', 'url'); + + foreach ($this->moduleDataSetup->getConnection()->fetchAll($select) as $link) { + $this->addHost($link['link_url']); + } + + $select = $this->moduleDataSetup->getConnection() + ->select() + ->from( + $this->moduleDataSetup->getTable('downloadable_link'), + ['sample_url'] + ) + ->where('sample_type = ?', 'url'); + + foreach ($this->moduleDataSetup->getConnection()->fetchAll($select) as $link) { + $this->addHost($link['sample_url']); + } + } + + if ($this->moduleDataSetup->tableExists('downloadable_sample')) { + $select = $this->moduleDataSetup->getConnection() + ->select() + ->from( + $this->moduleDataSetup->getTable('downloadable_sample'), + ['sample_url'] + ) + ->where('sample_type = ?', 'url'); + + foreach ($this->moduleDataSetup->getConnection()->fetchAll($select) as $link) { + $this->addHost($link['sample_url']); + } + } + + $this->domainManager->addDomains($this->whitelist); + } + + /** + * Add stores and website urls from store scope + * + * @param Store $scope + */ + private function addStoreAndWebsiteUrlsFromScope(Store $scope) + { + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_WEB, false)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_WEB, true)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_LINK, false)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_LINK, true)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_DIRECT_LINK, false)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_DIRECT_LINK, true)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_MEDIA, false)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_MEDIA, true)); + + try { + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_STATIC, false)); + $this->addHost($scope->getBaseUrl(UrlInterface::URL_TYPE_STATIC, true)); + } catch (\UnexpectedValueException $e) {} //@codingStandardsIgnoreLine + + try { + $website = $scope->getWebsite(); + } catch (NoSuchEntityException $e) { + return; + } + + if ($website) { + $this->addHost($website->getConfig(Store::XML_PATH_SECURE_BASE_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_UNSECURE_BASE_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_SECURE_BASE_LINK_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_UNSECURE_BASE_LINK_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_SECURE_BASE_MEDIA_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_UNSECURE_BASE_MEDIA_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_SECURE_BASE_STATIC_URL)); + $this->addHost($website->getConfig(Store::XML_PATH_UNSECURE_BASE_STATIC_URL)); + } + } + + /** + * Add host to whitelist + * + * @param string $url + */ + private function addHost($url) + { + if (!is_string($url)) { + return; + } + + $host = $this->uriHandler->parse($url)->getHost(); + if ($host && !in_array($host, $this->whitelist)) { + $this->whitelist[] = $host; + } + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index caf2f7745a3dd..0000000000000 --- a/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tableName = $this->schemaSetup->getTable('catalog_product_index_price_downlod_tmp'); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/AdminDownloadableProductActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/AdminDownloadableProductActionGroup.xml index f40f5cb47e4dd..2d2cdd969ac9d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/AdminDownloadableProductActionGroup.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/AdminDownloadableProductActionGroup.xml @@ -51,18 +51,20 @@ </annotations> <arguments> <argument name="link" defaultValue="downloadableLink"/> + <argument name="index" type="string" defaultValue="1"/> </arguments> <click selector="{{AdminProductDownloadableSection.linksAddLinkButton}}" stepKey="clickLinkAddLinkButton"/> <waitForPageLoad stepKey="waitForPageLoad"/> - <fillField userInput="{{link.title}}" selector="{{AdminProductDownloadableSection.addLinkTitleInput('1')}}" stepKey="fillDownloadableLinkTitle"/> - <fillField userInput="{{link.price}}" selector="{{AdminProductDownloadableSection.addLinkPriceInput('1')}}" stepKey="fillDownloadableLinkPrice"/> - <selectOption userInput="{{link.file_type}}" selector="{{AdminProductDownloadableSection.addLinkFileTypeSelector('1')}}" stepKey="selectDownloadableLinkFileType"/> - <selectOption userInput="{{link.sample_type}}" selector="{{AdminProductDownloadableSection.addLinkSampleTypeSelector('1')}}" stepKey="selectDownloadableLinkSampleType"/> - <selectOption userInput="{{link.shareable}}" selector="{{AdminProductDownloadableSection.addLinkShareableSelector('1')}}" stepKey="selectDownloadableLinkShareable"/> - <checkOption selector="{{AdminProductDownloadableSection.addLinkIsUnlimitedDownloads('1')}}" stepKey="checkDownloadableLinkUnlimited"/> - <fillField userInput="{{link.file}}" selector="{{AdminProductDownloadableSection.addLinkFileUrlInput('1')}}" stepKey="fillDownloadableLinkFileUrl"/> - <attachFile userInput="{{link.sample}}" selector="{{AdminProductDownloadableSection.addLinkSampleUploadFile('1')}}" stepKey="attachDownloadableLinkUploadSample"/> + <fillField userInput="{{link.title}}" selector="{{AdminProductDownloadableSection.addLinkTitleInput(index)}}" stepKey="fillDownloadableLinkTitle"/> + <fillField userInput="{{link.price}}" selector="{{AdminProductDownloadableSection.addLinkPriceInput(index)}}" stepKey="fillDownloadableLinkPrice"/> + <selectOption userInput="{{link.file_type}}" selector="{{AdminProductDownloadableSection.addLinkFileTypeSelector(index)}}" stepKey="selectDownloadableLinkFileType"/> + <selectOption userInput="{{link.sample_type}}" selector="{{AdminProductDownloadableSection.addLinkSampleTypeSelector(index)}}" stepKey="selectDownloadableLinkSampleType"/> + <selectOption userInput="{{link.shareable}}" selector="{{AdminProductDownloadableSection.addLinkShareableSelector(index)}}" stepKey="selectDownloadableLinkShareable"/> + <checkOption selector="{{AdminProductDownloadableSection.addLinkIsUnlimitedDownloads(index)}}" stepKey="checkDownloadableLinkUnlimited"/> + <fillField userInput="{{link.file}}" selector="{{AdminProductDownloadableSection.addLinkFileUrlInput(index)}}" stepKey="fillDownloadableLinkFileUrl"/> + <attachFile userInput="{{link.sample}}" selector="{{AdminProductDownloadableSection.addLinkSampleUploadFile(index)}}" stepKey="attachDownloadableLinkUploadSample"/> + <waitForPageLoad stepKey="waitForPageLoadAfterFillingOutForm" /> </actionGroup> <!--Add a downloadable sample file--> diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml new file mode 100644 index 0000000000000..565439655138e --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.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="StorefrontOpenDownloadableLinkActionGroup"> + <arguments> + <argument name="linkId" type="string"/> + </arguments> + <amOnPage url="{{StorefrontDownloadableLinkPage.url(linkId)}}" stepKey="openDownloadableLink"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml new file mode 100644 index 0000000000000..25ac45317fe42 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.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="StorefrontOpenDownloadableSampleActionGroup"> + <arguments> + <argument name="sampleId" type="string"/> + </arguments> + <amOnPage url="{{StorefrontDownloadableSamplePage.url(sampleId)}}" stepKey="openDownloadableSample"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml index 08f1c2349357d..eb3ad674a0fdf 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -63,6 +63,12 @@ <data key="file_type">URL</data> <data key="file">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> </entity> + <entity name="DownloadableSample" type="downloadable_sample"> + <data key="title" unique="suffix">downloadableSampleUrl</data> + <data key="sort_order">1</data> + <data key="sample_type">url</data> + <data key="sample_url">http://example.com</data> + </entity> <entity name="ApiDownloadableLink" type="downloadable_link"> <data key="title" unique="suffix">Api Downloadable Link</data> <data key="price">2.00</data> @@ -72,4 +78,4 @@ <data key="sort_order">0</data> <data key="link_url">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 1a6be43b38d2c..2986532ef1138 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -74,6 +74,21 @@ <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> <requiredEntity type="downloadable_link">apiDownloadableLink</requiredEntity> </entity> + <entity name="ApiDownloadableProductUnderscoredSku" type="product"> + <data key="sku" unique="suffix">api_downloadable_product</data> + <data key="type_id">downloadable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Downloadable Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-downloadable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + <requiredEntity type="downloadable_link">apiDownloadableLink</requiredEntity> + </entity> <entity name="DownloadableProductWithTwoLink100" type="product"> <data key="sku" unique="suffix">downloadableproduct</data> <data key="type_id">downloadable</data> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml new file mode 100644 index 0000000000000..b26bbb7af5a35 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateDownloadableSample" dataType="downloadable_sample" type="create" auth="adminOauth" url="/V1/products/{sku}/downloadable-links/samples" method="POST"> + <contentType>application/json</contentType> + <object dataType="downloadable_sample" key="sample"> + <field key="title">string</field> + <field key="sort_order">integer</field> + <field key="sample_type">string</field> + <field key="sample_file">string</field> + <field key="sample_file_content">sample_file_content</field> + <field key="sample_url">string</field> + </object> + <field key="isGlobalScopeContent">boolean</field> + </operation> +</operations> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml new file mode 100644 index 0000000000000..7ab6b211d7441 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml @@ -0,0 +1,13 @@ +<?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="StorefrontDownloadableLinkPage" url="downloadable/download/linkSample/link_id/{{id}}/" area="storefront" module="Magento_Downloadable" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml new file mode 100644 index 0000000000000..0d588faa777c0 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml @@ -0,0 +1,13 @@ +<?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="StorefrontDownloadableSamplePage" url="downloadable/download/sample/sample_id/{{id}}/" area="storefront" module="Magento_Downloadable" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml new file mode 100644 index 0000000000000..7b9d205d19dc5 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.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="StorefrontProductPage" url="/{{var1}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> + <section name="StorefrontDownloadableProductSection" /> + </page> +</pages> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml index a1db2d4d94941..dc2a58be138e7 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml @@ -12,5 +12,9 @@ <element name="downloadableLinkBlock" type="text" selector="//div[contains(@class, 'field downloads required')]//span[text()='Downloadable Links']"/> <element name="downloadableLinkLabel" type="text" selector="//label[contains(., '{{title}}')]" parameterized="true" timeout="30"/> <element name="downloadableLinkByTitle" type="input" selector="//*[@id='downloadable-links-list']/*[contains(.,'{{title}}')]//input" parameterized="true" timeout="30"/> + <element name="downloadableLinkSampleByTitle" type="text" selector="//label[contains(., '{{title}}')]/a[contains(@class, 'sample link')]" parameterized="true"/> + <element name="downloadableSampleLabel" type="text" selector="//a[contains(.,normalize-space('{{title}}'))]" parameterized="true" timeout="30"/> + <element name="downloadableLinkSelectAllCheckbox" type="checkbox" selector="#links_all" /> + <element name="downloadableLinkSelectAllLabel" type="text" selector="label[for='links_all']" /> </section> </sections> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml index 64f33b01e668f..a7ce96ddf1fde 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml @@ -19,12 +19,14 @@ <group value="Downloadable"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> <argument name="product" value="DownloadableProduct"/> </actionGroup> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> </after> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml index a7acdfded29b6..d95ddaf12470d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml @@ -18,7 +18,12 @@ <testCaseId value="MC-114"/> <group value="Downloadable"/> </annotations> - + <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="enableAdminAccountSharing"/> + </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="setStoreDefaultConfig"/> + </after> <!-- Create a downloadable product --> <!-- Replacing steps in base AdminAddDefaultVideoSimpleProductTest --> <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml index 5d7e4518525f7..4f07334640cf3 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml index 0ae2c1254be01..54a2ff606f384 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml index eadefabb8bbcb..24741c281d7f6 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminCreateDownloadableProductWithLinkTest"> + <test name="AdminCreateDownloadableProductWithDefaultSetLinksTest"> <annotations> <features value="Catalog"/> <stories value="Create Downloadable Product"/> @@ -20,13 +20,17 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> - + <!-- Reindex and clear page cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> <!-- Login as admin --> <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -72,6 +76,8 @@ <!-- Save product --> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!-- Find downloadable product in grid --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml index 8bd4305e6b358..06cf31b763f1c 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml new file mode 100644 index 0000000000000..f2e4bdfb4890f --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml @@ -0,0 +1,45 @@ +<?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="AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest" extends="AdminCreateDownloadableProductWithLinkTest"> + <annotations> + <stories value="Create Downloadable Product"/> + <title value="Create Downloadable Product with invalid domain link url"/> + <description value="Admin should not be able to create downloadable product with invalid domain link url"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-18282"/> + <useCaseId value="MC-17700"/> + <group value="Downloadable"/> + </annotations> + <before> + <remove keyForRemoval="addDownloadableDomain" /> + </before> + <actionGroup ref="addDownloadableProductLink" stepKey="addDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + <argument name="index" value="0"/> + </actionGroup> + <actionGroup ref="SaveProductFormNoSuccessCheck" stepKey="saveProduct"/> + <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="Link URL's domain is not in list of downloadable_domains in env.php." stepKey="seeLinkUrlInvalidMessage" after="saveProduct" /> + <magentoCLI stepKey="addDownloadableDomain2" command="downloadable:domains:add static.magento.com" after="seeLinkUrlInvalidMessage" /> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductFormAgain" after="addDownloadableDomain2"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + <checkOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable" after="fillDownloadableProductFormAgain"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkIsLinksPurchasedSeparately" after="checkIsDownloadable"/> + <actionGroup ref="addDownloadableProductLink" stepKey="addDownloadableProductLinkAgain" after="checkIsLinksPurchasedSeparately"> + <argument name="link" value="downloadableLink"/> + <argument name="index" value="0"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductAfterAddingDomainToWhitelist" after="addDownloadableProductLinkAgain" /> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="scrollToLinks"/> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="selectProductLink"/> + <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$52.99" stepKey="assertProductPriceInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml index d8bd641e84e55..e43b8f94c7a3d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminCreateDownloadableProductWithDefaultSetLinksTest"> + <test name="AdminCreateDownloadableProductWithLinkTest"> <annotations> <features value="Catalog"/> <stories value="Create Downloadable Product"/> @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml index 3efd4b8ab276f..fb6a48254fa8d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml index 07f7c40bb3560..5e3fe6836f7e9 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml index 275e72b2ec8cb..fb59d51831bae 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml index f326a047c32b5..af9487e3e6a23 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml index 8e33a082d0ba2..dd7e3331a0ed2 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml @@ -20,6 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -27,6 +28,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml index d3c2d6e5d71a4..07124ea4846be 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml @@ -18,6 +18,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="DownloadableProductWithTwoLink" stepKey="createDownloadableProduct"> @@ -31,6 +32,7 @@ </createData> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml new file mode 100644 index 0000000000000..20c1acaf8d612 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml @@ -0,0 +1,83 @@ +<?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="AdminDownloadableProductTypeSwitchingToConfigurableProductTest" extends="AdminSimpleProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Product type switching"/> + <title value="Downloadable product type switching on editing to configurable product"/> + <description value="Downloadable product type switching on editing to configurable product"/> + <testCaseId value="MC-17957"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <!-- Open Dropdown and select downloadable product option --> + <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection" after="waitForSimpleProductPageLoad"/> + <uncheckOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkOptionIsDownloadable" after="openDownloadableSection"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeightForProduct" after="checkOptionIsDownloadable"/> + <actionGroup ref="saveProductForm" stepKey="saveDownloadableProductForm" after="selectWeightForProduct"/> + </test> + <test name="AdminSimpleProductTypeSwitchingToDownloadableProductTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Product type switching"/> + <title value="Simple product type switching on editing to downloadable product"/> + <description value="Simple product type switching on editing to downloadable product"/> + <testCaseId value="MC-17956"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Change product type to Downloadable--> + <comment userInput="Change product type to Downloadable" stepKey="commentCreateDownloadable"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForDownloadableProductPageLoad"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has no weight" stepKey="selectNoWeightForProduct"/> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkOptionPurchaseSeparately"/> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveDownloadableProductForm"/> + <!--Assert downloadable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertDownloadableProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> + <!--Assert downloadable product on storefront--> + <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontDownloadableProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertDownloadableProductInStock"/> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinksInStorefront"/> + <seeElement selector="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeDownloadableLink" /> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml index 3ee6cef47738b..3597c12e82df0 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml @@ -19,9 +19,11 @@ <group value="Downloadable"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> </after> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml index d8bbbb2b4d62b..0d98862d9a5e7 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml @@ -18,6 +18,12 @@ <testCaseId value="MC-207"/> <group value="Downloadable"/> </annotations> + <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="enableAdminAccountSharing"/> + </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="setStoreDefaultConfig"/> + </after> <!-- Create a downloadable product --> <!-- Replacing steps in base AdminRemoveDefaultVideoSimpleProductTest --> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml index 66177b6875dd9..b5f437996d69b 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml @@ -19,6 +19,7 @@ <group value="Downloadable"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="product"/> <createData entity="ApiDownloadableProduct" stepKey="product"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="product"/> @@ -27,6 +28,9 @@ <requiredEntity createDataKey="product"/> </createData> </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="delete"/> + </after> </test> <test name="AdvanceCatalogSearchDownloadableBySkuTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> <annotations> @@ -39,7 +43,8 @@ <group value="Downloadable"/> </annotations> <before> - <createData entity="ApiDownloadableProduct" stepKey="product"/> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="product"/> + <createData entity="ApiDownloadableProductUnderscoredSku" stepKey="product"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="product"/> </createData> @@ -47,6 +52,9 @@ <requiredEntity createDataKey="product"/> </createData> </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="delete"/> + </after> </test> <test name="AdvanceCatalogSearchDownloadableByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> <annotations> @@ -59,6 +67,7 @@ <group value="Downloadable"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="product"/> <createData entity="ApiDownloadableProduct" stepKey="product"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="product"/> @@ -67,6 +76,9 @@ <requiredEntity createDataKey="product"/> </createData> </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="delete"/> + </after> </test> <test name="AdvanceCatalogSearchDownloadableByShortDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> <annotations> @@ -79,6 +91,7 @@ <group value="Downloadable"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="product"/> <createData entity="ApiDownloadableProduct" stepKey="product"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="product"/> @@ -87,6 +100,9 @@ <requiredEntity createDataKey="product"/> </createData> </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="delete"/> + </after> </test> <test name="AdvanceCatalogSearchDownloadableByPriceTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> <annotations> @@ -99,6 +115,7 @@ <group value="Downloadable"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="product"/> <createData entity="ApiDownloadableProduct" stepKey="product"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="product"/> @@ -107,5 +124,8 @@ <requiredEntity createDataKey="product"/> </createData> </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="delete"/> + </after> </test> </tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml new file mode 100644 index 0000000000000..274dd39468a2b --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml @@ -0,0 +1,110 @@ +<?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="EditDownloadableProductWithSeparateLinksFromCartTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Downloadable Product"/> + <title value="Edit downloadable product with separate links from cart test"/> + <description value="Product price should remain correct when editing downloadable product with separate links from cart."/> + <severity value="MAJOR"/> + <group value="Downloadable"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- Create downloadable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Fill downloadable link information before the creation link --> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + + <!-- Links can be purchased separately --> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" + stepKey="checkOptionPurchaseSeparately"/> + + <!-- Add first downloadable link --> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addFirstDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + + <!-- Add second downloadable link --> + <actionGroup ref="addDownloadableProductLink" stepKey="addSecondDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Step 1: Navigate to store front Product page as guest --> + <amOnPage url="/{{DownloadableProduct.sku}}.html" + stepKey="amOnStorefrontProductPage"/> + + <!-- Step 2: Add checkbox for first link --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" + stepKey="selectProductLink"/> + + <!-- Step 3: Add the Product to cart --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="DownloadableProduct"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Step 4: Open cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="openShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$51.99" + stepKey="assertProductPriceInCart"/> + + <!-- Step 5: Edit Product in cart --> + <click selector="{{CheckoutCartProductSection.nthEditButton('1')}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForEditPage"/> + + <!-- Step 6: Make sure Product price is correct --> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="51.99" stepKey="checkPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml index b960d15b2fdf1..b9773415059ec 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16011"/> </annotations> <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> <magentoCLI command="config:set {{EnableGuestCheckoutWithDownloadableItems.path}} {{EnableGuestCheckoutWithDownloadableItems.value}}" stepKey="enableGuestCheckoutWithDownloadableItems" /> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> @@ -28,6 +29,7 @@ </createData> </before> <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <magentoCLI command="config:set {{DisableGuestCheckoutWithDownloadableItems.path}} {{DisableGuestCheckoutWithDownloadableItems.value}}" stepKey="disableGuestCheckoutWithDownloadableItems" /> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml new file mode 100644 index 0000000000000..a86fd544d24d6 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml @@ -0,0 +1,128 @@ +<?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="ManualSelectAllDownloadableLinksDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Downloadable Product"/> + <title value="Manual select all downloadable links downloadable product test"/> + <description value="Manually selecting all downloadable links must change 'Select/Unselect all' button label to 'Unselect all', and 'Select all' otherwise"/> + <severity value="MAJOR"/> + <group value="Downloadable"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- Create downloadable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Fill downloadable link information before the creation link --> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + + <!-- Links can be purchased separately --> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" + stepKey="checkOptionPurchaseSeparately"/> + + <!-- Add first downloadable link --> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addFirstDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + + <!-- Add second downloadable link --> + <actionGroup ref="addDownloadableProductLink" stepKey="addSecondDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Step 1: Navigate to store front Product page as guest --> + <amOnPage url="/{{DownloadableProduct.sku}}.html" + stepKey="amOnStorefrontProductPage"/> + + <!-- Step 2: Check first downloadable link checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" + stepKey="selectFirstCheckbox"/> + + <!-- Step 3: Check second downloadable link checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" + stepKey="selectSecondCheckbox"/> + + <!-- Step 4: Grab "Select/Unselect All" button label text --> + <grabTextFrom + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllLabel}}" + stepKey="grabUnselectAllButtonText"/> + + <!-- Step 5: Assert that 'Select/Unselect all' button text is 'Unselect all' after manually checking all checkboxes --> + <assertEquals + message="Assert that 'Select/Unselect all' button text is 'Unselect all' after manually checking all checkboxes" + stepKey="assertButtonTextOne"> + <expectedResult type="string">Unselect all</expectedResult> + <actualResult type="string">{$grabUnselectAllButtonText}</actualResult> + </assertEquals> + + <!-- Step 6: Uncheck second downloadable link checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" + stepKey="unselectSecondCheckbox"/> + + <!-- Step 7: Grab "Select/Unselect All" button label text --> + <grabTextFrom + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllLabel}}" + stepKey="grabSelectAllButtonText"/> + + <!-- Step 8: Assert that 'Select/Unselect all' button text is 'Select all' after manually unchecking one checkbox --> + <assertEquals + message="Assert that 'Select/Unselect all' button text is 'Select all' after manually unchecking one checkbox" + stepKey="assertButtonTextTwo"> + <expectedResult type="string">Select all</expectedResult> + <actualResult type="string">{$grabSelectAllButtonText}</actualResult> + </assertEquals> + + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml index 4864d11c884bc..94fca6f507637 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml @@ -20,6 +20,13 @@ <group value="WYSIWYGDisabled"/> </annotations> + <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com" before="loginAsAdmin"/> + </before> + <after> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com" before="logout"/> + </after> + <!-- A Cms page containing the New Products Widget gets created here via extends --> <!-- Create a Downloadable product to appear in the widget --> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml new file mode 100644 index 0000000000000..f9ca6fea09cf0 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml @@ -0,0 +1,107 @@ +<?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="SelectAllDownloadableLinksDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Downloadable Product"/> + <title value="Select all downloadable links downloadable product test"/> + <description value="All the downloadable links must be selected or unselected when anyone click on select all or unselect all checkbox respectively."/> + <severity value="MAJOR"/> + <group value="Downloadable"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- Create downloadable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Fill downloadable link information before the creation link --> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + + <!-- Links can be purchased separately --> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" + stepKey="checkOptionPurchaseSeparately"/> + + <!-- Add first downloadable link --> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addFirstDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + + <!-- Add second downloadable link --> + <actionGroup ref="addDownloadableProductLink" stepKey="addSecondDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Step 1: Navigate to store front Product page as guest --> + <amOnPage url="/{{DownloadableProduct.sku}}.html" + stepKey="amOnStorefrontProductPage"/> + + <!-- Step 2: click on select all checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllCheckbox}}" + stepKey="selectAllProductLink"/> + + <!-- Step 3: Make sure that all product links are checked --> + <seeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeFirstCheckboxChecked"/> + + <seeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="seeSecondCheckboxChecked"/> + + <!-- Step 4: click again on select all checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllCheckbox}}" + stepKey="unselectAllProductLink"/> + + <!-- Step 5: Make sure that all product links are unchecked --> + <dontSeeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeFirstCheckboxUnChecked"/> + + <dontSeeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="seeSecondCheckboxUnChecked"/> + + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..e4a5bd732a83c --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest.xml @@ -0,0 +1,36 @@ +<?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="StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Downloadable product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search Downloadable product with product that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-252"/> + <group value="Downloadable"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <magentoCLI command="downloadable:domains:add example.com static.magento.com" before="product" stepKey="addDownloadableDomain"/> + <createData entity="ApiDownloadableProduct" stepKey="product"/> + <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink2"> + <requiredEntity createDataKey="product"/> + </createData> + </before> + <after> + <magentoCLI command="downloadable:domains:remove example.com static.magento.com" stepKey="removeDownloadableDomain"/> + </after> + </test> + </tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml new file mode 100644 index 0000000000000..ba2e3453a6d99 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml @@ -0,0 +1,120 @@ +<?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="VerifyDisableDownloadableProductSamplesAreNotAccessibleTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Downloadable product"/> + <title value="Samples of Downloadable Products are not accessible, if product is disabled"/> + <description value="Samples of Downloadable Products are not accessible, if product is disabled"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-15845"/> + <useCaseId value="MC-14824"/> + <group value="downloadable"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create downloadable product --> + <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add downloadable link --> + <createData entity="downloadableLink1" stepKey="addDownloadableLink"> + <requiredEntity createDataKey="createProduct"/> + </createData> + + <!-- Add downloadable sample --> + <createData entity="DownloadableSample" stepKey="addDownloadableSample"> + <requiredEntity createDataKey="createProduct"/> + </createData> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteDownloadableProduct"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Admin logout --> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Open Downloadable product from precondition on Storefront --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Sample url is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableSampleLabel(DownloadableSample.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableSampleLabel(DownloadableSample.title)}}" stepKey="clickDownloadableSample"/> + + <!-- Grab Sample id --> + <switchToNextTab stepKey="switchToSampleTab"/> + <grabFromCurrentUrl regex="~/sample_id/(\d+)/~" stepKey="grabDownloadableSampleId"/> + <closeTab stepKey="closeSampleTab"/> + + <!-- Link Sample url is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLink"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLink1.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink1.title)}}" stepKey="clickDownloadableLinkSample"/> + + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkId"/> + <closeTab stepKey="closeLinkSampleTab"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Open Downloadable product from precondition --> + <actionGroup ref="goToProductPageViaID" stepKey="openProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + + <!-- Change status of product to "Disable" and save it --> + <actionGroup ref="AdminSetProductDisabled" stepKey="disableProduct"/> + <actionGroup ref="saveProductForm" stepKey="clickSaveProduct"/> + + <!-- Assert product is disable on Storefront --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + + <!-- Navigate to Link Sample url on Storefront --> + <actionGroup ref="StorefrontOpenDownloadableLinkActionGroup" stepKey="openDownloadableLinkSample"> + <argument name="linkId" value="{$grabDownloadableLinkId}"/> + </actionGroup> + + <!-- Link Sample url is not accessible. You are redirected to Home Page --> + <seeInCurrentUrl url="{{StorefrontHomePage.url}}" stepKey="seeRedirectToHomePage"/> + + <!-- Navigate to Sample url on Storefront --> + <actionGroup ref="StorefrontOpenDownloadableSampleActionGroup" stepKey="openDownloadableSample"> + <argument name="sampleId" value="{$grabDownloadableSampleId}"/> + </actionGroup> + + <!-- Sample url is not accessible. You are redirected to Home Page --> + <seeInCurrentUrl url="{{StorefrontHomePage.url}}" stepKey="seeHomePage"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/DownloadableTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/DownloadableTest.php index 25a5d86b0385c..55353c16b4727 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/DownloadableTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/DownloadableTest.php @@ -7,6 +7,9 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; +/** + * Unit tests for \Magento\Downloadable\Controller\Adminhtml\Product\Initialization\Helper\Plugin\Downloadable. + */ class DownloadableTest extends \PHPUnit\Framework\TestCase { /** @@ -34,12 +37,20 @@ class DownloadableTest extends \PHPUnit\Framework\TestCase */ private $extensionAttributesMock; + /** + * @var \Magento\Downloadable\Model\Product\Type|\Magento\Catalog\Api\Data\ProductExtensionInterface + */ + private $downloadableProductTypeMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); $this->productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, - ['setDownloadableData', 'getExtensionAttributes', '__wakeup'] + ['setDownloadableData', 'getExtensionAttributes', '__wakeup', 'getTypeInstance'] ); $this->subjectMock = $this->createMock( \Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::class @@ -62,6 +73,10 @@ protected function setUp() $sampleBuilderMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample\Builder::class) ->disableOriginalConstructor() ->getMock(); + $this->downloadableProductTypeMock = $this->createPartialMock( + \Magento\Downloadable\Model\Product\Type::class, + ['getLinks', 'getSamples'] + ); $this->downloadablePlugin = new \Magento\Downloadable\Controller\Adminhtml\Product\Initialization\Helper\Plugin\Downloadable( $this->requestMock, @@ -86,6 +101,11 @@ public function testAfterInitializeWithNoDataToSave($downloadable) $this->productMock->expects($this->once()) ->method('getExtensionAttributes') ->willReturn($this->extensionAttributesMock); + $this->productMock->expects($this->exactly(2)) + ->method('getTypeInstance') + ->willReturn($this->downloadableProductTypeMock); + $this->downloadableProductTypeMock->expects($this->once())->method('getLinks')->willReturn([]); + $this->downloadableProductTypeMock->expects($this->once())->method('getSamples')->willReturn([]); $this->extensionAttributesMock->expects($this->once()) ->method('setDownloadableProductLinks') ->with([]); diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Link/ContentValidatorTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Link/ContentValidatorTest.php index 2639c22ff2ca2..152e3699f9691 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Link/ContentValidatorTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Link/ContentValidatorTest.php @@ -5,8 +5,12 @@ */ namespace Magento\Downloadable\Test\Unit\Model\Link; +use Magento\Downloadable\Helper\File; use Magento\Downloadable\Model\Link\ContentValidator; +/** + * Unit tests for Magento\Downloadable\Model\Link\ContentValidator. + */ class ContentValidatorTest extends \PHPUnit\Framework\TestCase { /** @@ -24,6 +28,11 @@ class ContentValidatorTest extends \PHPUnit\Framework\TestCase */ protected $urlValidatorMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $domainValidatorMock; + /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -34,13 +43,34 @@ class ContentValidatorTest extends \PHPUnit\Framework\TestCase */ protected $sampleFileMock; + /** + * @var File|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileMock; + + /** + * @inheritdoc + */ protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->fileValidatorMock = $this->createMock(\Magento\Downloadable\Model\File\ContentValidator::class); $this->urlValidatorMock = $this->createMock(\Magento\Framework\Url\Validator::class); + $this->domainValidatorMock = $this->createMock(\Magento\Downloadable\Model\Url\DomainValidator::class); $this->linkFileMock = $this->createMock(\Magento\Downloadable\Api\Data\File\ContentInterface::class); $this->sampleFileMock = $this->createMock(\Magento\Downloadable\Api\Data\File\ContentInterface::class); - $this->validator = new ContentValidator($this->fileValidatorMock, $this->urlValidatorMock); + $this->fileMock = $this->createMock(File::class); + + $this->validator = $objectManager->getObject( + ContentValidator::class, + [ + 'fileContentValidator' => $this->fileValidatorMock, + 'urlValidator' => $this->urlValidatorMock, + 'fileHelper' => $this->fileMock, + 'domainValidator' => $this->domainValidatorMock, + ] + ); } public function testIsValid() @@ -60,6 +90,7 @@ public function testIsValid() ]; $this->fileValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $this->urlValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); + $this->domainValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $linkMock = $this->getLinkMock($linkData); $this->assertTrue($this->validator->isValid($linkMock)); } @@ -80,6 +111,7 @@ public function testIsValidSkipLinkContent() ]; $this->fileValidatorMock->expects($this->once())->method('isValid')->will($this->returnValue(true)); $this->urlValidatorMock->expects($this->never())->method('isValid')->will($this->returnValue(true)); + $this->domainValidatorMock->expects($this->never())->method('isValid')->will($this->returnValue(true)); $linkMock = $this->getLinkMock($linkData); $this->assertTrue($this->validator->isValid($linkMock, false)); } @@ -100,6 +132,7 @@ public function testIsValidSkipSampleContent() ]; $this->fileValidatorMock->expects($this->never())->method('isValid')->will($this->returnValue(true)); $this->urlValidatorMock->expects($this->once())->method('isValid')->will($this->returnValue(true)); + $this->domainValidatorMock->expects($this->once())->method('isValid')->will($this->returnValue(true)); $linkMock = $this->getLinkMock($linkData); $this->assertTrue($this->validator->isValid($linkMock, true, false)); } @@ -123,6 +156,7 @@ public function testIsValidThrowsExceptionIfSortOrderIsInvalid($sortOrder) ]; $this->fileValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $this->urlValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); + $this->domainValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $contentMock = $this->getLinkMock($linkContentData); $this->validator->isValid($contentMock); } @@ -158,6 +192,7 @@ public function testIsValidThrowsExceptionIfPriceIsInvalid($price) ]; $this->fileValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $this->urlValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); + $this->domainValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $contentMock = $this->getLinkMock($linkContentData); $this->validator->isValid($contentMock); } @@ -191,6 +226,7 @@ public function testIsValidThrowsExceptionIfNumberOfDownloadsIsInvalid($numberOf 'sample_type' => 'file', ]; $this->urlValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); + $this->domainValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $this->fileValidatorMock->expects($this->any())->method('isValid')->will($this->returnValue(true)); $contentMock = $this->getLinkMock($linkContentData); $this->validator->isValid($contentMock); @@ -223,45 +259,29 @@ protected function getLinkMock(array $linkData) 'isShareable', 'getNumberOfDownloads', 'getLinkType', - 'getLinkFile' + 'getLinkFile', ] ) ->getMockForAbstractClass(); - $linkMock->expects($this->any())->method('getTitle')->will($this->returnValue( - $linkData['title'] - )); - $linkMock->expects($this->any())->method('getPrice')->will($this->returnValue( - $linkData['price'] - )); - $linkMock->expects($this->any())->method('getSortOrder')->will($this->returnValue( - $linkData['sort_order'] - )); - $linkMock->expects($this->any())->method('isShareable')->will($this->returnValue( - $linkData['shareable'] - )); - $linkMock->expects($this->any())->method('getNumberOfDownloads')->will($this->returnValue( - $linkData['number_of_downloads'] - )); - $linkMock->expects($this->any())->method('getLinkType')->will($this->returnValue( - $linkData['link_type'] - )); - $linkMock->expects($this->any())->method('getLinkFile')->will($this->returnValue( - $this->linkFileMock - )); + $linkMock->expects($this->any())->method('getTitle')->will($this->returnValue($linkData['title'])); + $linkMock->expects($this->any())->method('getPrice')->will($this->returnValue($linkData['price'])); + $linkMock->expects($this->any())->method('getSortOrder')->will($this->returnValue($linkData['sort_order'])); + $linkMock->expects($this->any())->method('isShareable')->will($this->returnValue($linkData['shareable'])); + $linkMock->expects($this->any())->method('getNumberOfDownloads')->will( + $this->returnValue($linkData['number_of_downloads']) + ); + $linkMock->expects($this->any())->method('getLinkType')->will($this->returnValue($linkData['link_type'])); + $linkMock->expects($this->any())->method('getLinkFile')->will($this->returnValue($this->linkFileMock)); if (isset($linkData['link_url'])) { - $linkMock->expects($this->any())->method('getLinkUrl')->will($this->returnValue( - $linkData['link_url'] - )); + $linkMock->expects($this->any())->method('getLinkUrl')->will($this->returnValue($linkData['link_url'])); } if (isset($linkData['sample_url'])) { - $linkMock->expects($this->any())->method('getSampleUrl')->will($this->returnValue( - $linkData['sample_url'] - )); + $linkMock->expects($this->any())->method('getSampleUrl')->will($this->returnValue($linkData['sample_url'])); } if (isset($linkData['sample_type'])) { - $linkMock->expects($this->any())->method('getSampleType')->will($this->returnValue( - $linkData['sample_type'] - )); + $linkMock->expects($this->any())->method('getSampleType')->will( + $this->returnValue($linkData['sample_type']) + ); } if (isset($linkData['link_file_content'])) { $linkMock->expects($this->any())->method('getLinkFileContent')->willReturn($linkData['link_file_content']); @@ -270,9 +290,8 @@ protected function getLinkMock(array $linkData) $linkMock->expects($this->any())->method('getSampleFileContent') ->willReturn($linkData['sample_file_content']); } - $linkMock->expects($this->any())->method('getSampleFile')->will($this->returnValue( - $this->sampleFileMock - )); + $linkMock->expects($this->any())->method('getSampleFile')->will($this->returnValue($this->sampleFileMock)); + return $linkMock; } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/LinkRepositoryTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/LinkRepositoryTest.php index 821f251929f8b..25f720f27150c 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/LinkRepositoryTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/LinkRepositoryTest.php @@ -98,7 +98,9 @@ protected function setUp() \Magento\Framework\Json\EncoderInterface::class ); $this->linkFactoryMock = $this->createPartialMock(\Magento\Downloadable\Model\LinkFactory::class, ['create']); - $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + $this->productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ '__wakeup', 'getTypeId', 'setDownloadableData', @@ -107,8 +109,9 @@ protected function setUp() 'getStoreId', 'getStore', 'getWebsiteIds', - 'getData' - ]); + 'getData', + ] + ); $this->service = new \Magento\Downloadable\Model\LinkRepository( $this->repositoryMock, $this->productTypeMock, @@ -162,7 +165,8 @@ protected function getLinkMock(array $linkData) 'getNumberOfDownloads', 'getIsShareable', 'getLinkUrl', - 'getLinkFile' + 'getLinkFile', + 'hasSampleType', ] ) ->getMockForAbstractClass(); @@ -309,12 +313,15 @@ public function testUpdate() $storeMock = $this->createMock(\Magento\Store\Model\Store::class); $storeMock->expects($this->any())->method('getWebsiteId')->will($this->returnValue($websiteId)); $this->productMock->expects($this->any())->method('getStore')->will($this->returnValue($storeMock)); - $existingLinkMock = $this->createPartialMock(\Magento\Downloadable\Model\Link::class, [ + $existingLinkMock = $this->createPartialMock( + \Magento\Downloadable\Model\Link::class, + [ '__wakeup', 'getId', 'load', - 'getProductId' - ]); + 'getProductId', + ] + ); $this->linkFactoryMock->expects($this->once())->method('create')->will($this->returnValue($existingLinkMock)); $linkMock = $this->getLinkMock($linkData); $this->contentValidatorMock->expects($this->any())->method('isValid')->with($linkMock) @@ -371,12 +378,15 @@ public function testUpdateWithExistingFile() $storeMock = $this->createMock(\Magento\Store\Model\Store::class); $storeMock->expects($this->any())->method('getWebsiteId')->will($this->returnValue($websiteId)); $this->productMock->expects($this->any())->method('getStore')->will($this->returnValue($storeMock)); - $existingLinkMock = $this->createPartialMock(\Magento\Downloadable\Model\Link::class, [ + $existingLinkMock = $this->createPartialMock( + \Magento\Downloadable\Model\Link::class, + [ '__wakeup', 'getId', 'load', - 'getProductId' - ]); + 'getProductId', + ] + ); $this->linkFactoryMock->expects($this->once())->method('create')->will($this->returnValue($existingLinkMock)); $linkMock = $this->getLinkMock($linkData); $this->contentValidatorMock->expects($this->any())->method('isValid')->with($linkMock) @@ -436,6 +446,8 @@ public function testUpdateThrowsExceptionIfTitleIsEmptyAndScopeIsGlobal() 'price' => 10.1, 'number_of_downloads' => 100, 'is_shareable' => true, + 'link_type' => 'url', + 'link_url' => 'https://google.com', ]; $this->repositoryMock->expects($this->any())->method('get')->with($productSku, true) ->will($this->returnValue($this->productMock)); @@ -501,10 +513,12 @@ public function testGetList() 'sample_file' => '/r/o/rock.melody.ogg', 'link_type' => 'url', 'link_url' => 'http://link.url', - 'link_file' => null + 'link_file' => null, ]; - $linkMock = $this->createPartialMock(\Magento\Downloadable\Model\Link::class, [ + $linkMock = $this->createPartialMock( + \Magento\Downloadable\Model\Link::class, + [ 'getId', 'getStoreTitle', 'getTitle', @@ -519,8 +533,9 @@ public function testGetList() 'getSampleUrl', 'getLinkType', 'getLinkFile', - 'getLinkUrl' - ]); + 'getLinkUrl', + ] + ); $linkInterfaceMock = $this->createMock(\Magento\Downloadable\Api\Data\LinkInterface::class); diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Sample/ContentValidatorTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Sample/ContentValidatorTest.php index c863fb7ad62ff..4a32a45859cec 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Sample/ContentValidatorTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Sample/ContentValidatorTest.php @@ -6,7 +6,11 @@ namespace Magento\Downloadable\Test\Unit\Model\Sample; use Magento\Downloadable\Model\Sample\ContentValidator; +use Magento\Downloadable\Helper\File; +/** + * Unit tests for Magento\Downloadable\Model\Sample\ContentValidator. + */ class ContentValidatorTest extends \PHPUnit\Framework\TestCase { /** @@ -34,12 +38,31 @@ class ContentValidatorTest extends \PHPUnit\Framework\TestCase */ protected $sampleFileMock; + /** + * @var File|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileMock; + + /** + * @inheritdoc + */ protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->fileValidatorMock = $this->createMock(\Magento\Downloadable\Model\File\ContentValidator::class); $this->urlValidatorMock = $this->createMock(\Magento\Framework\Url\Validator::class); $this->sampleFileMock = $this->createMock(\Magento\Downloadable\Api\Data\File\ContentInterface::class); - $this->validator = new ContentValidator($this->fileValidatorMock, $this->urlValidatorMock); + $this->fileMock = $this->createMock(File::class); + + $this->validator = $objectManager->getObject( + ContentValidator::class, + [ + 'fileContentValidator' => $this->fileValidatorMock, + 'urlValidator' => $this->urlValidatorMock, + 'fileHelper' => $this->fileMock, + ] + ); } public function testIsValid() @@ -94,28 +117,29 @@ public function getInvalidSortOrder() protected function getSampleContentMock(array $sampleContentData) { $contentMock = $this->createMock(\Magento\Downloadable\Api\Data\SampleInterface::class); - $contentMock->expects($this->any())->method('getTitle')->will($this->returnValue( - $sampleContentData['title'] - )); - - $contentMock->expects($this->any())->method('getSortOrder')->will($this->returnValue( - $sampleContentData['sort_order'] - )); - $contentMock->expects($this->any())->method('getSampleType')->will($this->returnValue( - $sampleContentData['sample_type'] - )); + $contentMock->expects($this->any())->method('getTitle')->will( + $this->returnValue($sampleContentData['title']) + ); + + $contentMock->expects($this->any())->method('getSortOrder')->will( + $this->returnValue($sampleContentData['sort_order']) + ); + $contentMock->expects($this->any())->method('getSampleType')->will( + $this->returnValue($sampleContentData['sample_type']) + ); if (isset($sampleContentData['sample_url'])) { - $contentMock->expects($this->any())->method('getSampleUrl')->will($this->returnValue( - $sampleContentData['sample_url'] - )); + $contentMock->expects($this->any())->method('getSampleUrl')->will( + $this->returnValue($sampleContentData['sample_url']) + ); } if (isset($sampleContentData['sample_file_content'])) { $contentMock->expects($this->any())->method('getSampleFileContent') ->willReturn($sampleContentData['sample_file_content']); } - $contentMock->expects($this->any())->method('getSampleFile')->will($this->returnValue( - $this->sampleFileMock - )); + $contentMock->expects($this->any())->method('getSampleFile')->will( + $this->returnValue($this->sampleFileMock) + ); + return $contentMock; } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/SampleRepositoryTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/SampleRepositoryTest.php index 8e13bd83b039e..f1674c6838a2d 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/SampleRepositoryTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/SampleRepositoryTest.php @@ -149,25 +149,26 @@ protected function getSampleMock(array $sampleData) $sampleMock->expects($this->any())->method('getId')->willReturn($sampleData['id']); } $sampleMock->expects($this->any())->method('getTitle')->will($this->returnValue($sampleData['title'])); - $sampleMock->expects($this->any())->method('getSortOrder')->will($this->returnValue( - $sampleData['sort_order'] - )); + $sampleMock->expects($this->any())->method('getSortOrder')->will( + $this->returnValue($sampleData['sort_order']) + ); if (isset($sampleData['sample_type'])) { - $sampleMock->expects($this->any())->method('getSampleType')->will($this->returnValue( - $sampleData['sample_type'] - )); + $sampleMock->expects($this->any())->method('getSampleType')->will( + $this->returnValue($sampleData['sample_type']) + ); } if (isset($sampleData['sample_url'])) { - $sampleMock->expects($this->any())->method('getSampleUrl')->will($this->returnValue( - $sampleData['sample_url'] - )); + $sampleMock->expects($this->any())->method('getSampleUrl')->will( + $this->returnValue($sampleData['sample_url']) + ); } if (isset($sampleData['sample_file'])) { - $sampleMock->expects($this->any())->method('getSampleFile')->will($this->returnValue( - $sampleData['sample_file'] - )); + $sampleMock->expects($this->any())->method('getSampleFile')->will( + $this->returnValue($sampleData['sample_file']) + ); } + return $sampleMock; } @@ -353,6 +354,8 @@ public function testUpdateThrowsExceptionIfTitleIsEmptyAndScopeIsGlobal() 'id' => $sampleId, 'title' => '', 'sort_order' => 1, + 'sample_type' => 'url', + 'sample_url' => 'https://google.com', ]; $this->repositoryMock->expects($this->any())->method('get')->with($productSku, true) ->will($this->returnValue($this->productMock)); @@ -414,10 +417,12 @@ public function testGetList() 'sort_order' => 21, 'sample_type' => 'file', 'sample_url' => null, - 'sample_file' => '/r/o/rock.melody.ogg' + 'sample_file' => '/r/o/rock.melody.ogg', ]; - $sampleMock = $this->createPartialMock(\Magento\Downloadable\Model\Sample::class, [ + $sampleMock = $this->createPartialMock( + \Magento\Downloadable\Model\Sample::class, + [ 'getId', 'getStoreTitle', 'getTitle', @@ -426,8 +431,9 @@ public function testGetList() 'getSampleUrl', 'getSortOrder', 'getData', - '__wakeup' - ]); + '__wakeup', + ] + ); $sampleInterfaceMock = $this->createMock(\Magento\Downloadable\Api\Data\SampleInterface::class); diff --git a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/SamplesTest.php b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/SamplesTest.php new file mode 100644 index 0000000000000..2f5b47a1d86b5 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/SamplesTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Downloadable\Test\Unit\Ui\DataProvider\Product\Form\Modifier\Data; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Data\Samples; +use \Magento\Framework\Escaper; +use Magento\Downloadable\Model\Product\Type; +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Downloadable\Helper\File as DownloadableFile; +use Magento\Framework\UrlInterface; +use Magento\Catalog\Api\Data\ProductInterface; + +/** + * Test class to cover Sample Modifier + * + * Class \Magento\Downloadable\Test\Unit\Ui\DataProvider\Product\Form\Modifier\Data\SampleTest + */ +class SamplesTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var LocatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $locatorMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @var DownloadableFile|\PHPUnit_Framework_MockObject_MockObject + */ + private $downloadableFileMock; + + /** + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlBuilderMock; + + /** + * @var ProductInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var Samples + */ + private $samples; + + /** + * @return void + */ + protected function setUp() + { + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->productMock = $this->getMockBuilder(ProductInterface::class) + ->setMethods(['getSamplesTitle', 'getId', 'getTypeId']) + ->getMockForAbstractClass(); + $this->locatorMock = $this->createMock(LocatorInterface::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->escaperMock = $this->createMock(Escaper::class); + $this->downloadableFileMock = $this->createMock(DownloadableFile::class); + $this->urlBuilderMock = $this->createMock(UrlInterface::class); + $this->samples = $this->objectManagerHelper->getObject( + Samples::class, + [ + 'escaper' => $this->escaperMock, + 'locator' => $this->locatorMock, + 'scopeConfig' => $this->scopeConfigMock, + 'downloadableFile' => $this->downloadableFileMock, + 'urlBuilder' => $this->urlBuilderMock + ] + ); + } + + /** + * Test getSamplesTitle() + * + * @param int|null $id + * @param string $typeId + * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectedGetTitle + * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectedGetValue + * @return void + * @dataProvider getSamplesTitleDataProvider + */ + public function testGetSamplesTitle($id, $typeId, $expectedGetTitle, $expectedGetValue) + { + $title = 'My Title'; + $this->locatorMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->productMock->expects($this->once()) + ->method('getId') + ->willReturn($id); + $this->productMock->expects($this->any()) + ->method('getTypeId') + ->willReturn($typeId); + $this->productMock->expects($expectedGetTitle) + ->method('getSamplesTitle') + ->willReturn($title); + $this->scopeConfigMock->expects($expectedGetValue) + ->method('getValue') + ->willReturn($title); + + /* Assert Result */ + $this->assertEquals($title, $this->samples->getSamplesTitle()); + } + + /** + * @return array + */ + public function getSamplesTitleDataProvider() + { + return [ + [ + 'id' => 1, + 'typeId' => Type::TYPE_DOWNLOADABLE, + 'expectedGetTitle' => $this->once(), + 'expectedGetValue' => $this->never(), + ], + [ + 'id' => null, + 'typeId' => Type::TYPE_DOWNLOADABLE, + 'expectedGetTitle' => $this->never(), + 'expectedGetValue' => $this->once(), + ], + [ + 'id' => 1, + 'typeId' => 'someType', + 'expectedGetTitle' => $this->never(), + 'expectedGetValue' => $this->once(), + ], + [ + 'id' => null, + 'typeId' => 'someType', + 'expectedGetTitle' => $this->never(), + 'expectedGetValue' => $this->once(), + ], + ]; + } +} diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php index f29708cc9a2c6..0a3ea2fc6ba19 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php @@ -13,6 +13,7 @@ use Magento\Framework\UrlInterface; use Magento\Downloadable\Model\Link as LinkModel; use Magento\Downloadable\Api\Data\LinkInterface; +use Magento\Framework\Exception\ValidatorException; /** * Class Links @@ -155,7 +156,7 @@ protected function addSampleFile(array $linkData, LinkInterface $link) $sampleFile = $link->getSampleFile(); if ($sampleFile) { $file = $this->downloadableFile->getFilePath($this->linkModel->getBaseSamplePath(), $sampleFile); - if ($this->downloadableFile->ensureFileInFilesystem($file)) { + if ($this->isLinkFileValid($file)) { $linkData['sample']['file'][0] = [ 'file' => $sampleFile, 'name' => $this->downloadableFile->getFileFromPathFile($sampleFile), @@ -184,7 +185,7 @@ protected function addLinkFile(array $linkData, LinkInterface $link) $linkFile = $link->getLinkFile(); if ($linkFile) { $file = $this->downloadableFile->getFilePath($this->linkModel->getBasePath(), $linkFile); - if ($this->downloadableFile->ensureFileInFilesystem($file)) { + if ($this->isLinkFileValid($file)) { $linkData['file'][0] = [ 'file' => $linkFile, 'name' => $this->downloadableFile->getFileFromPathFile($linkFile), @@ -201,6 +202,21 @@ protected function addLinkFile(array $linkData, LinkInterface $link) return $linkData; } + /** + * Check that Links File or Sample is valid. + * + * @param string $file + * @return bool + */ + private function isLinkFileValid(string $file): bool + { + try { + return $this->downloadableFile->ensureFileInFilesystem($file); + } catch (ValidatorException $e) { + return false; + } + } + /** * Return formatted price with two digits after decimal point * diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Samples.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Samples.php index b000de487b775..988f429de1d87 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Samples.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Samples.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Locator\LocatorInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Downloadable\Helper\File as DownloadableFile; +use Magento\Framework\Exception\ValidatorException; use Magento\Framework\UrlInterface; use Magento\Downloadable\Api\Data\SampleInterface; @@ -136,7 +137,7 @@ protected function addSampleFile(array $sampleData, SampleInterface $sample) $sampleFile = $sample->getSampleFile(); if ($sampleFile) { $file = $this->downloadableFile->getFilePath($this->sampleModel->getBasePath(), $sampleFile); - if ($this->downloadableFile->ensureFileInFilesystem($file)) { + if ($this->isSampleFileValid($file)) { $sampleData['file'][0] = [ 'file' => $sampleFile, 'name' => $this->downloadableFile->getFileFromPathFile($sampleFile), @@ -152,4 +153,19 @@ protected function addSampleFile(array $sampleData, SampleInterface $sample) return $sampleData; } + + /** + * Check that Sample file is valid. + * + * @param string $file + * @return bool + */ + private function isSampleFileValid(string $file): bool + { + try { + return $this->downloadableFile->ensureFileInFilesystem($file); + } catch (ValidatorException $e) { + return false; + } + } } diff --git a/app/code/Magento/Downloadable/etc/db_schema.xml b/app/code/Magento/Downloadable/etc/db_schema.xml index ccbefa4fb3992..ee7b3c5683ea1 100644 --- a/app/code/Magento/Downloadable/etc/db_schema.xml +++ b/app/code/Magento/Downloadable/etc/db_schema.xml @@ -233,7 +233,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_downlod_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_downlod_tmp" resource="default" engine="innodb" comment="Temporary Indexer Table for price of downloadable products"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> diff --git a/app/code/Magento/Downloadable/etc/di.xml b/app/code/Magento/Downloadable/etc/di.xml index 4e9b0b55afb0b..3dc592958588c 100644 --- a/app/code/Magento/Downloadable/etc/di.xml +++ b/app/code/Magento/Downloadable/etc/di.xml @@ -92,6 +92,7 @@ <preference for="Magento\Downloadable\Api\Data\File\ContentUploaderInterface" type="Magento\Downloadable\Model\File\ContentUploader" /> <preference for="Magento\Downloadable\Model\Product\TypeHandler\TypeHandlerInterface" type="Magento\Downloadable\Model\Product\TypeHandler\TypeHandler" /> <preference for="Magento\Downloadable\Api\Data\DownloadableOptionInterface" type="Magento\Downloadable\Model\DownloadableOption" /> + <preference for="Magento\Downloadable\Api\DomainManagerInterface" type="Magento\Downloadable\Model\DomainManager"/> <type name="Magento\Framework\EntityManager\Operation\ExtensionPool"> <arguments> <argument name="extensionActions" xsi:type="array"> @@ -164,4 +165,25 @@ <argument name="connectionName" xsi:type="string">indexer</argument> </arguments> </type> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="addDomainsCommand" xsi:type="object">Magento\Downloadable\Console\Command\DomainsAddCommand</item> + <item name="removeDomainsCommand" xsi:type="object">Magento\Downloadable\Console\Command\DomainsRemoveCommand</item> + <item name="showDomainsCommand" xsi:type="object">Magento\Downloadable\Console\Command\DomainsShowCommand</item> + </argument> + </arguments> + </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="links_exist" xsi:type="string">catalog_product</item> + <item name="links_purchased_separately" xsi:type="string">catalog_product</item> + <item name="links_title" xsi:type="string">catalog_product</item> + <item name="samples_title" xsi:type="string">catalog_product</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Downloadable/i18n/en_US.csv b/app/code/Magento/Downloadable/i18n/en_US.csv index 87427bf483966..1e96413aa08a1 100644 --- a/app/code/Magento/Downloadable/i18n/en_US.csv +++ b/app/code/Magento/Downloadable/i18n/en_US.csv @@ -118,3 +118,5 @@ Downloads,Downloads "Use Content-Disposition","Use Content-Disposition" "Disable Guest Checkout if Cart Contains Downloadable Items","Disable Guest Checkout if Cart Contains Downloadable Items" "Guest checkout will only work with shareable.","Guest checkout will only work with shareable." +"Link URL's domain is not in list of downloadable_domains in env.php.","Link URL's domain is not in list of downloadable_domains in env.php." +"Sample URL's domain is not in list of downloadable_domains in env.php.","Sample URL's domain is not in list of downloadable_domains in env.php." diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml index 8d471a1e49e7f..17d5b73f49506 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml @@ -18,14 +18,14 @@ </legend><br /> <?php $_links = $block->getLinks(); ?> <?php $_isRequired = $block->getLinkSelectionRequired(); ?> - <div class="field admin__field link <?php if ($_isRequired) { echo ' required _required'; } ?>"> + <div class="field admin__field link<?php if ($_isRequired) { echo ' _required'; } ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($block->getLinksTitle()) ?></span></label> <div class="control admin__field-control" id="downloadable-links-list"> <?php foreach ($_links as $_link) : ?> <div class="nested admin__field-option"> <?php if ($_linksPurchasedSeparately) : ?> <input type="checkbox" - class="admin__control-checkbox checkbox<?php if ($_isRequired) :?> validate-one-required-by-name<?php endif; ?> product downloadable link" + class="admin__control-checkbox checkbox<?php if ($_isRequired) :?> required-entry<?php endif; ?> product downloadable link" name="links[]" id="links_<?= $block->escapeHtmlAttr($_link->getId()) ?>" value="<?= $block->escapeHtmlAttr($_link->getId()) ?>" <?= $block->escapeHtml($block->getLinkCheckedValue($_link)) ?> @@ -81,7 +81,7 @@ require(['prototype'], function(){ } } //]]> - + }); </script> <?php endif;?> diff --git a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js index 09a5ad1afa9ec..8bdea0b3a70b6 100644 --- a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js +++ b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js @@ -12,12 +12,17 @@ define([ ], function ($) { 'use strict'; + /** + * Downloadable widget + */ $.widget('mage.downloadable', { options: { priceHolderSelector: '.price-box' }, - /** @inheritdoc */ + /** + * @inheritdoc + */ _create: function () { var self = this; @@ -38,6 +43,8 @@ define([ }); } }); + + this._reloadPrice(); }, /** @@ -63,6 +70,32 @@ define([ } } }); + + this.reloadAllCheckText(); + }, + + /** + * Reload all-elements-checkbox's label + * @private + */ + reloadAllCheckText: function () { + var allChecked = true, + allElementsCheck = $(this.options.allElements), + allElementsLabel = $('label[for="' + allElementsCheck.attr('id') + '"] > span'); + + $(this.options.linkElement).each(function () { + if (!this.checked) { + allChecked = false; + } + }); + + if (allChecked) { + allElementsLabel.text(allElementsCheck.attr('data-checked')); + allElementsCheck.prop('checked', true); + } else { + allElementsLabel.text(allElementsCheck.attr('data-notchecked')); + allElementsCheck.prop('checked', false); + } } }); diff --git a/app/code/Magento/DownloadableGraphQl/Model/Cart/BuyRequest/DownloadableLinksDataProvider.php b/app/code/Magento/DownloadableGraphQl/Model/Cart/BuyRequest/DownloadableLinksDataProvider.php index 18f883b61516d..5f159971e4a19 100644 --- a/app/code/Magento/DownloadableGraphQl/Model/Cart/BuyRequest/DownloadableLinksDataProvider.php +++ b/app/code/Magento/DownloadableGraphQl/Model/Cart/BuyRequest/DownloadableLinksDataProvider.php @@ -40,12 +40,7 @@ public function execute(array $cartItemData): array if (isset($cartItemData['data']) && isset($cartItemData['data']['sku'])) { $sku = $cartItemData['data']['sku']; - - try { - $product = $this->productRepository->get($sku); - } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__('Could not find specified product.')); - } + $product = $this->productRepository->get($sku); if ($product->getLinksPurchasedSeparately() && isset($cartItemData['downloadable_product_links'])) { $downloadableLinks = $cartItemData['downloadable_product_links']; diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index db452d1e5ace1..2226f1acd8501 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -43,20 +43,20 @@ enum DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get th } type DownloadableProductLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { - id: Int @deprecated(reason: "This information shoud not be exposed on frontend") + id: Int @deprecated(reason: "This information should not be exposed on frontend") title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") price: Float @doc(description: "The price of the downloadable product") sample_url: String @doc(description: "URL to the downloadable sample") - is_shareable: Boolean @deprecated(reason: "This information shoud not be exposed on frontend") - number_of_downloads: Int @deprecated(reason: "This information shoud not be exposed on frontend") + is_shareable: Boolean @deprecated(reason: "This information should not be exposed on frontend") + number_of_downloads: Int @deprecated(reason: "This information should not be exposed on frontend") link_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_file: String @deprecated(reason: "`sample_url` serves to get the downloadable sample") } type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { - id: Int @deprecated(reason: "This information shoud not be exposed on frontend") + id: Int @deprecated(reason: "This information should not be exposed on frontend") title: String @doc(description: "The display name of the sample") sort_order: Int @doc(description: "A number indicating the sort order") sample_url: String @doc(description: "URL to the downloadable sample") diff --git a/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php b/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php index e03964bd2c386..c9cdf52f55dd1 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php +++ b/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php @@ -8,12 +8,14 @@ namespace Magento\DownloadableImportExport\Model\Import\Product\Type; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\Downloadable\Model\Url\DomainValidator; use Magento\Framework\EntityManager\MetadataPool; use \Magento\Store\Model\Store; /** * Class Downloadable * + * phpcs:disable Magento2.Commenting.ConstantsPHPDocFormatting * @SuppressWarnings(PHPMD.TooManyFields) */ class Downloadable extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType @@ -101,6 +103,10 @@ class Downloadable extends \Magento\CatalogImportExport\Model\Import\Product\Typ const ERROR_COLS_IS_EMPTY = 'emptyOptions'; + private const ERROR_LINK_URL_NOT_IN_DOMAIN_WHITELIST = 'linkUrlNotInDomainWhitelist'; + + private const ERROR_SAMPLE_URL_NOT_IN_DOMAIN_WHITELIST = 'sampleUrlNotInDomainWhitelist'; + /** * Validation failure message template definitions * @@ -111,7 +117,11 @@ class Downloadable extends \Magento\CatalogImportExport\Model\Import\Product\Typ self::ERROR_GROUP_TITLE_NOT_FOUND => 'Group titles not found for downloadable products', self::ERROR_OPTION_NO_TITLE => 'Option no title', self::ERROR_MOVE_FILE => 'Error move file', - self::ERROR_COLS_IS_EMPTY => 'Missing sample and links data for the downloadable product' + self::ERROR_COLS_IS_EMPTY => 'Missing sample and links data for the downloadable product', + self::ERROR_LINK_URL_NOT_IN_DOMAIN_WHITELIST => + 'Link URL\'s domain is not in list of downloadable_domains in env.php.', + self::ERROR_SAMPLE_URL_NOT_IN_DOMAIN_WHITELIST => + 'Sample URL\'s domain is not in list of downloadable_domains in env.php.' ]; /** @@ -244,6 +254,11 @@ class Downloadable extends \Magento\CatalogImportExport\Model\Import\Product\Typ */ protected $downloadableHelper; + /** + * @var DomainValidator + */ + private $domainValidator; + /** * Downloadable constructor * @@ -253,6 +268,7 @@ class Downloadable extends \Magento\CatalogImportExport\Model\Import\Product\Typ * @param array $params * @param \Magento\DownloadableImportExport\Helper\Uploader $uploaderHelper * @param \Magento\DownloadableImportExport\Helper\Data $downloadableHelper + * @param DomainValidator $domainValidator * @param MetadataPool $metadataPool */ public function __construct( @@ -262,12 +278,14 @@ public function __construct( array $params, \Magento\DownloadableImportExport\Helper\Uploader $uploaderHelper, \Magento\DownloadableImportExport\Helper\Data $downloadableHelper, + DomainValidator $domainValidator, MetadataPool $metadataPool = null ) { parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params, $metadataPool); $this->parameters = $this->_entityModel->getParameters(); $this->_resource = $resource; $this->uploaderHelper = $uploaderHelper; + $this->domainValidator = $domainValidator; $this->downloadableHelper = $downloadableHelper; } @@ -336,17 +354,36 @@ public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) */ protected function isRowValidSample(array $rowData) { - $result = false; - if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES]) - && $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '' - && $this->sampleGroupTitle($rowData) == '') { - $this->_entityModel->addRowError(self::ERROR_GROUP_TITLE_NOT_FOUND, $this->rowNum); + $hasSampleLinkData = ( + isset($rowData[self::COL_DOWNLOADABLE_SAMPLES]) && + $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '' + ); + + if (!$hasSampleLinkData) { + return false; + } + + $sampleData = $this->prepareSampleData($rowData[static::COL_DOWNLOADABLE_SAMPLES]); + + if ($this->sampleGroupTitle($rowData) == '') { $result = true; + $this->_entityModel->addRowError(self::ERROR_GROUP_TITLE_NOT_FOUND, $this->rowNum); } - if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES]) - && $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '') { - $result = $this->isTitle($this->prepareSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES])); + + $result = $result ?? $this->isTitle($sampleData); + + foreach ($sampleData as $link) { + if ($this->hasDomainNotInWhitelist($link, 'link_type', 'link_url')) { + $this->_entityModel->addRowError(static::ERROR_LINK_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); + $result = true; + } + + if ($this->hasDomainNotInWhitelist($link, 'sample_type', 'sample_url')) { + $this->_entityModel->addRowError(static::ERROR_SAMPLE_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); + $result = true; + } } + return $result; } @@ -358,19 +395,36 @@ protected function isRowValidSample(array $rowData) */ protected function isRowValidLink(array $rowData) { - $result = false; - if (isset($rowData[self::COL_DOWNLOADABLE_LINKS]) && - $rowData[self::COL_DOWNLOADABLE_LINKS] != '' && - $this->linksAdditionalAttributes($rowData, 'group_title', self::DEFAULT_GROUP_TITLE) == '' - ) { + $hasLinkData = ( + isset($rowData[self::COL_DOWNLOADABLE_LINKS]) && + $rowData[self::COL_DOWNLOADABLE_LINKS] != '' + ); + + if (!$hasLinkData) { + return false; + } + + $linkData = $this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS]); + + if ($this->linksAdditionalAttributes($rowData, 'group_title', self::DEFAULT_GROUP_TITLE) == '') { $this->_entityModel->addRowError(self::ERROR_GROUP_TITLE_NOT_FOUND, $this->rowNum); $result = true; } - if (isset($rowData[self::COL_DOWNLOADABLE_LINKS]) && - $rowData[self::COL_DOWNLOADABLE_LINKS] != '' - ) { - $result = $this->isTitle($this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS])); + + $result = $result ?? $this->isTitle($linkData); + + foreach ($linkData as $link) { + if ($this->hasDomainNotInWhitelist($link, 'link_type', 'link_url')) { + $this->_entityModel->addRowError(static::ERROR_LINK_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); + $result = true; + } + + if ($this->hasDomainNotInWhitelist($link, 'sample_type', 'sample_url')) { + $this->_entityModel->addRowError(static::ERROR_SAMPLE_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); + $result = true; + } } + return $result; } @@ -733,6 +787,7 @@ protected function prepareSampleData($rowCol, $entityId = null) $rowCol ); foreach ($options as $option) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $result[] = array_merge( $this->dataSample, ['product_id' => $entityId], @@ -757,6 +812,7 @@ protected function prepareLinkData($rowCol, $entityId = null) $rowCol ); foreach ($options as $option) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $result[] = array_merge( $this->dataLink, ['product_id' => $entityId], @@ -829,6 +885,7 @@ protected function parseSampleOption($values) /** * Uploading files into the "downloadable/files" media folder. + * * Return a new file name if the same file is already exists. * * @param string $fileName @@ -861,4 +918,23 @@ protected function clear() $this->productIds = []; return $this; } + + /** + * Does link contain url not in whitelist? + * + * @param array $link + * @param string $linkTypeKey + * @param string $linkUrlKey + * @return bool + */ + private function hasDomainNotInWhitelist(array $link, string $linkTypeKey, string $linkUrlKey): bool + { + return ( + isset($link[$linkTypeKey]) && + $link[$linkTypeKey] === 'url' && + isset($link[$linkUrlKey]) && + strlen($link[$linkUrlKey]) && + !$this->domainValidator->isValid($link[$linkUrlKey]) + ); + } } diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php index 72f4086c1c56b..7af7bf447c45a 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php @@ -4,16 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Attribute add/edit form options tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Options; use Magento\Store\Model\ResourceModel\Store\Collection; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; /** + * Attribute add/edit form options tab + * * @api * @since 100.0.2 */ @@ -61,6 +59,7 @@ public function __construct( /** * Is true only for system attributes which use source model + * * Option labels and position for such attributes are kept in source model and thus cannot be overridden * * @return bool @@ -96,12 +95,16 @@ public function getStoresSortedBySortOrder() { $stores = $this->getStores(); if (is_array($stores)) { - usort($stores, function ($storeA, $storeB) { - if ($storeA->getSortOrder() == $storeB->getSortOrder()) { - return $storeA->getId() < $storeB->getId() ? -1 : 1; + usort( + $stores, + function ($storeA, $storeB) { + if ($storeA->getSortOrder() == $storeB->getSortOrder()) { + return $storeA->getId() < $storeB->getId() ? -1 : 1; + } + + return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; } - return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1; - }); + ); } return $stores; } @@ -130,12 +133,14 @@ public function getOptionValues() } /** - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * Preparing values of attribute options + * + * @param AbstractAttribute $attribute * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection * @return array */ protected function _prepareOptionValues( - \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, + AbstractAttribute $attribute, $optionCollection ) { $type = $attribute->getFrontendInput(); @@ -149,6 +154,41 @@ protected function _prepareOptionValues( $values = []; $isSystemAttribute = is_array($optionCollection); + if ($isSystemAttribute) { + $values = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); + } else { + $optionCollection->setPageSize(200); + $pageCount = $optionCollection->getLastPageNumber(); + $currentPage = 1; + while ($currentPage <= $pageCount) { + $optionCollection->clear(); + $optionCollection->setCurPage($currentPage); + $values = array_merge( + $values, + $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues) + ); + $currentPage++; + } + } + + return $values; + } + + /** + * Return prepared values of system or user defined attribute options + * + * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection + * @param bool $isSystemAttribute + * @param string $inputType + * @param array $defaultValues + */ + private function getPreparedValues( + $optionCollection, + bool $isSystemAttribute, + string $inputType, + array $defaultValues + ) { + $values = []; foreach ($optionCollection as $option) { $bunch = $isSystemAttribute ? $this->_prepareSystemAttributeOptionValues( $option, @@ -169,12 +209,13 @@ protected function _prepareOptionValues( /** * Retrieve option values collection + * * It is represented by an array in case of system attribute * - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * @param AbstractAttribute $attribute * @return array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ - protected function _getOptionValuesCollection(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute) + protected function _getOptionValuesCollection(AbstractAttribute $attribute) { if ($this->canManageOptionDefaultOnly()) { $options = $this->_universalFactory->create( @@ -226,7 +267,7 @@ protected function _prepareSystemAttributeOptionValues($option, $inputType, $def foreach ($this->getStores() as $store) { $storeId = $store->getId(); $value['store' . $storeId] = $storeId == - \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; + \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : ''; } return [$value]; diff --git a/app/code/Magento/Eav/Model/AttributeGroupSearchResults.php b/app/code/Magento/Eav/Model/AttributeGroupSearchResults.php new file mode 100644 index 0000000000000..100d5638a96da --- /dev/null +++ b/app/code/Magento/Eav/Model/AttributeGroupSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model; + +use Magento\Eav\Api\Data\AttributeGroupSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Attribute Group search results. + */ +class AttributeGroupSearchResults extends SearchResults implements AttributeGroupSearchResultsInterface +{ +} diff --git a/app/code/Magento/Eav/Model/AttributeSearchResults.php b/app/code/Magento/Eav/Model/AttributeSearchResults.php new file mode 100644 index 0000000000000..b82a27bbfea1a --- /dev/null +++ b/app/code/Magento/Eav/Model/AttributeSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model; + +use Magento\Eav\Api\Data\AttributeSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Eav Attribute search results. + */ +class AttributeSearchResults extends SearchResults implements AttributeSearchResultsInterface +{ +} diff --git a/app/code/Magento/Eav/Model/AttributeSetSearchResults.php b/app/code/Magento/Eav/Model/AttributeSetSearchResults.php new file mode 100644 index 0000000000000..46592efda5a17 --- /dev/null +++ b/app/code/Magento/Eav/Model/AttributeSetSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model; + +use Magento\Eav\Api\Data\AttributeSetSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Attribute Set search results. + */ +class AttributeSetSearchResults extends SearchResults implements AttributeSetSearchResultsInterface +{ +} diff --git a/app/code/Magento/Eav/Model/Config.php b/app/code/Magento/Eav/Model/Config.php index 0eecca21b0d54..20126d5146c35 100644 --- a/app/code/Magento/Eav/Model/Config.php +++ b/app/code/Magento/Eav/Model/Config.php @@ -7,12 +7,20 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Type; +use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\AbstractModel; use Magento\Framework\Serialize\SerializerInterface; /** + * EAV config model. + * * @api + * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ class Config @@ -25,6 +33,11 @@ class Config const ATTRIBUTES_CODES_CACHE_ID = 'EAV_ENTITY_ATTRIBUTES_CODES'; /**#@-*/ + /** + * Xml path to caching user defined eav attributes configuration. + */ + private const XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES = 'dev/caching/cache_user_defined_attributes'; + /**#@-*/ protected $_entityTypeData; @@ -116,6 +129,11 @@ class Config */ private $serializer; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * Cache of attributes per set * @@ -123,6 +141,20 @@ class Config */ private $attributesPerSet = []; + /** + * Is system attributes loaded flag. + * + * @var array + */ + private $isSystemAttributesLoaded = []; + + /** + * List of predefined system attributes for preload. + * + * @var array + */ + private $attributesForPreload; + /** * @param \Magento\Framework\App\CacheInterface $cache * @param \Magento\Eav\Model\Entity\TypeFactory $entityTypeFactory @@ -130,6 +162,8 @@ class Config * @param \Magento\Framework\App\Cache\StateInterface $cacheState * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param SerializerInterface $serializer + * @param ScopeConfigInterface $scopeConfig + * @param array $attributesForPreload * @codeCoverageIgnore */ public function __construct( @@ -138,7 +172,9 @@ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Type\CollectionFactory $entityTypeCollectionFactory, \Magento\Framework\App\Cache\StateInterface $cacheState, \Magento\Framework\Validator\UniversalFactory $universalFactory, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ScopeConfigInterface $scopeConfig = null, + $attributesForPreload = [] ) { $this->_cache = $cache; $this->_entityTypeFactory = $entityTypeFactory; @@ -146,6 +182,8 @@ public function __construct( $this->_cacheState = $cacheState; $this->_universalFactory = $universalFactory; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->attributesForPreload = $attributesForPreload; } /** @@ -487,7 +525,7 @@ public function getAttributes($entityType) * @param mixed $entityType * @param mixed $code * @return AbstractAttribute - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getAttribute($entityType, $code) { @@ -507,8 +545,152 @@ public function getAttribute($entityType, $code) return $this->attributes[$entityTypeCode][$code]; } + if (array_key_exists($entityTypeCode, $this->attributesForPreload) + && array_key_exists($code, $this->attributesForPreload[$entityTypeCode]) + ) { + $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); + } + if (isset($this->attributes[$entityTypeCode][$code])) { + \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); + return $this->attributes[$entityTypeCode][$code]; + } + + if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { + $attribute = $this->cacheUserDefinedAttribute($entityType, $entityTypeCode, $code); + } else { + $attribute = $this->initUserDefinedAttribute($entityType, $entityTypeCode, $code); + } + + \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); + return $attribute; + } + + /** + * Initialize predefined system attributes for preload. + * + * @param string $entityType + * @param array $systemAttributes + * @return $this|bool|void + * @throws LocalizedException + */ + private function initSystemAttributes($entityType, $systemAttributes) + { + $entityType = $this->getEntityType($entityType); + $entityTypeCode = $entityType->getEntityTypeCode(); + if (!empty($this->isSystemAttributesLoaded[$entityTypeCode])) { + return; + } + + $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-preload'; + if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { + $attributes = $this->serializer->unserialize($attributes); + if ($attributes) { + foreach ($attributes as $attribute) { + $attributeObject = $this->_createAttribute($entityType, $attribute); + $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); + } + return true; + } + } + + \Magento\Framework\Profiler::start('EAV: ' . __METHOD__, ['group' => 'EAV', 'method' => __METHOD__]); + + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributes */ + $attributes = $this->_universalFactory->create( + $entityType->getEntityAttributeCollection() + )->setEntityTypeFilter( + $entityType + )->addFieldToFilter( + 'attribute_code', + ['in' => array_keys($systemAttributes)] + )->getData(); + + $attributeData = []; + foreach ($attributes as $attribute) { + if (empty($attribute['attribute_model'])) { + $attribute['attribute_model'] = $entityType->getAttributeModel(); + } + $attributeObject = $this->_createAttribute($entityType, $attribute); + $this->saveAttribute($attributeObject, $entityTypeCode, $attributeObject->getAttributeCode()); + $attributeData[$attribute['attribute_code']] = $attributeObject->toArray(); + } + if ($this->isCacheEnabled()) { + $this->_cache->save( + $this->serializer->serialize($attributeData), + $cacheKey, + [ + \Magento\Eav\Model\Cache\Type::CACHE_TAG, + \Magento\Eav\Model\Entity\Attribute::CACHE_TAG + ] + ); + } + + \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); + $this->isSystemAttributesLoaded[$entityTypeCode] = true; + + return $this; + } + + /** + * Initialize user defined attribute from cache or cache it. + * + * @param string $entityType + * @param mixed $entityTypeCode + * @param string $code + * @return AbstractAttribute + * @throws LocalizedException + */ + private function cacheUserDefinedAttribute($entityType, $entityTypeCode, $code): AbstractAttribute + { + $cacheKey = self::ATTRIBUTES_CACHE_ID . '-attribute-' . $entityTypeCode . '-' . $code; + $attributeData = $this->isCacheEnabled() && ($attribute = $this->_cache->load($cacheKey)) + ? $this->serializer->unserialize($attribute) + : null; + if ($attributeData) { + if (isset($attributeData['attribute_id'])) { + $attribute = $this->_createAttribute($entityType, $attributeData); + } else { + $entityType = $this->getEntityType($entityType); + $attribute = $this->createAttribute($entityType->getAttributeModel()); + $attribute->setAttributeCode($code); + $attribute = $this->setAttributeData($attribute, $entityType); + } + } else { + $attribute = $this->createAttributeByAttributeCode($entityType, $code); + $this->_addAttributeReference( + $attribute->getAttributeId(), + $attribute->getAttributeCode(), + $entityTypeCode + ); + $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); + if ($this->isCacheEnabled()) { + $this->_cache->save( + $this->serializer->serialize($attribute->getData()), + $cacheKey, + [ + \Magento\Eav\Model\Cache\Type::CACHE_TAG, + \Magento\Eav\Model\Entity\Attribute::CACHE_TAG + ] + ); + } + } + + return $attribute; + } + + /** + * Initialize user defined attribute and save it to memory cache. + * + * @param mixed $entityType + * @param string $entityTypeCode + * @param string $code + * @return AbstractAttribute|null + * @throws LocalizedException + */ + private function initUserDefinedAttribute($entityType, $entityTypeCode, $code): ?AbstractAttribute + { $attributes = $this->loadAttributes($entityTypeCode); - $attribute = isset($attributes[$code]) ? $attributes[$code] : null; + $attribute = $attributes[$code] ?? null; if (!$attribute) { $attribute = $this->createAttributeByAttributeCode($entityType, $code); $this->_addAttributeReference( @@ -518,7 +700,7 @@ public function getAttribute($entityType, $code) ); $this->saveAttribute($attribute, $entityTypeCode, $attribute->getAttributeCode()); } - \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); + return $attribute; } @@ -639,7 +821,7 @@ protected function _createAttribute($entityType, $attributeData) $existsFullAttribute = $attribute->hasIsRequired(); $fullAttributeData = array_key_exists('is_required', $attributeData); - if ($existsFullAttribute || !$existsFullAttribute && !$fullAttributeData) { + if ($existsFullAttribute || (!$existsFullAttribute && !$fullAttributeData)) { return $attribute; } } @@ -708,6 +890,7 @@ public function importAttributesData($entityType, array $attributes) * @param string $entityType * @param string $attributeCode * @return AbstractAttribute + * @throws LocalizedException */ private function createAttributeByAttributeCode($entityType, $attributeCode) { @@ -723,13 +906,28 @@ private function createAttributeByAttributeCode($entityType, $attributeCode) $attribute->setAttributeCode($attributeCode); } + $attribute = $this->setAttributeData($attribute, $entityType); + + return $attribute; + } + + /** + * Set entity type id, backend type, is global to attribute. + * + * @param AbstractAttribute $attribute + * @param AbstractModel $entityType + * @return AbstractAttribute + */ + private function setAttributeData($attribute, $entityType): AbstractAttribute + { $entity = $entityType->getEntity(); - if ($entity instanceof \Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface + if ($entity instanceof ProviderInterface && in_array($attribute->getAttributeCode(), $entity->getDefaultAttributes(), true) ) { $attribute->setBackendType(AbstractAttribute::TYPE_STATIC)->setIsGlobal(1); } $attribute->setEntityType($entityType)->setEntityTypeId($entityType->getId()); + return $attribute; } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 1fd71e446e6bb..7649c89a07955 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -11,21 +11,22 @@ use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; +use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface as DefaultAttributesProvider; use Magento\Framework\App\Config\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\DuplicateException; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; -use Magento\Eav\Model\ResourceModel\Attribute\DefaultEntityAttributes\ProviderInterface as DefaultAttributesProvider; -use Magento\Framework\Model\ResourceModel\AbstractResource; -use Magento\Framework\App\ObjectManager; /** * Entity/Attribute/Model - entity abstract * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.TooManyFields) @@ -266,6 +267,8 @@ public function setConnection($connection) /** * Resource initialization * + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock + * * @return void */ protected function _construct() @@ -412,61 +415,44 @@ protected function _getConfig() * * @param string|int|Element $attribute * @return AbstractAttribute|false + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getAttribute($attribute) { /** @var $config \Magento\Eav\Model\Config */ $config = $this->_getConfig(); - if (is_numeric($attribute)) { - $attributeId = $attribute; - $attributeInstance = $config->getAttribute($this->getEntityType(), $attributeId); - if ($attributeInstance) { - $attributeCode = $attributeInstance->getAttributeCode(); - } - } elseif (is_string($attribute)) { - $attributeCode = $attribute; - $attributeInstance = $config->getAttribute($this->getEntityType(), $attributeCode); - if (!$attributeInstance->getAttributeCode() && in_array($attribute, $this->getDefaultAttributes())) { - $attributeInstance->setAttributeCode( - $attribute - )->setBackendType( - AbstractAttribute::TYPE_STATIC - )->setIsGlobal( - 1 - )->setEntity( - $this - )->setEntityType( - $this->getEntityType() - )->setEntityTypeId( - $this->getEntityType()->getId() - ); - } - } elseif ($attribute instanceof AbstractAttribute) { - $attributeInstance = $attribute; - $attributeCode = $attributeInstance->getAttributeCode(); + + $attributeInstance = $config->getAttribute($this->getEntityType(), $attribute); + + if (!$attributeInstance->getAttributeCode() && in_array($attribute, $this->getDefaultAttributes(), true)) { + $attributeInstance = clone $attributeInstance; + $attributeInstance->setData([]); + $attributeInstance->setAttributeCode( + $attribute + )->setBackendType( + AbstractAttribute::TYPE_STATIC + )->setIsGlobal( + 1 + )->setEntity( + $this + )->setEntityType( + $this->getEntityType() + )->setEntityTypeId( + $this->getEntityType()->getId() + ); } - if (empty($attributeInstance) - || !$attributeInstance instanceof AbstractAttribute - || !$attributeInstance->getId() - && !in_array($attributeInstance->getAttributeCode(), $this->getDefaultAttributes()) + if (!$attributeInstance instanceof AbstractAttribute + || (!$attributeInstance->getId() + && !in_array($attributeInstance->getAttributeCode(), $this->getDefaultAttributes(), true)) ) { return false; } - $attribute = $attributeInstance; - - if (!$attribute->getAttributeCode()) { - $attribute->setAttributeCode($attributeCode); - } - if (!$attribute->getAttributeModel()) { - $attribute->setAttributeModel($this->_getDefaultAttributeModel()); - } - - $this->addAttribute($attribute); + $this->addAttribute($attributeInstance); - return $attribute; + return $attributeInstance; } /** @@ -640,7 +626,7 @@ protected function _isApplicableAttribute($object, $attribute) public function walkAttributes($partMethod, array $args = [], $collectExceptionMessages = null) { $methodArr = explode('/', $partMethod); - switch (sizeof($methodArr)) { + switch (count($methodArr)) { case 1: $part = 'attribute'; $method = $methodArr[0]; @@ -687,6 +673,7 @@ public function walkAttributes($partMethod, array $args = [], $collectExceptionM } try { + // phpcs:disable Magento2.Functions.DiscouragedFunction $results[$attrCode] = call_user_func_array([$instance, $method], $args); } catch (\Magento\Eav\Model\Entity\Attribute\Exception $e) { if ($collectExceptionMessages) { @@ -826,9 +813,9 @@ public function getValueTablePrefix() $prefix = (string) $this->getEntityType()->getValueTablePrefix(); if (!empty($prefix)) { $this->_valueTablePrefix = $prefix; - /** - * entity type prefix include DB table name prefix - */ + /** + * entity type prefix include DB table name prefix + */ //$this->_resource->getTableName($prefix); } else { $this->_valueTablePrefix = $this->getEntityTable(); @@ -991,9 +978,9 @@ public function getDefaultAttributeSourceModel() /** * Load entity's attributes into the object * - * @param AbstractModel $object - * @param int $entityId - * @param array|null $attributes + * @param AbstractModel $object + * @param int $entityId + * @param array|null $attributes * @return $this */ public function load($object, $entityId, $attributes = []) @@ -1131,8 +1118,8 @@ protected function _getLoadAttributesSelect($object, $table) /** * Initialize attribute value for object * - * @param DataObject $object - * @param array $valueRow + * @param DataObject $object + * @param array $valueRow * @return $this */ protected function _setAttributeValue($object, $valueRow) @@ -1237,7 +1224,7 @@ protected function _getOrigObject($object) /** * Aggregate Data for attributes that will be deleted * - * @param array &$delete + * @param &array $delete * @param AbstractAttribute $attribute * @param AbstractEntity $object * @return void @@ -1248,6 +1235,7 @@ private function _aggregateDeleteData(&$delete, $attribute, $object) if (!isset($delete[$tableName])) { $delete[$tableName] = []; } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $delete[$tableName] = array_merge((array)$delete[$tableName], $valuesData); } } @@ -1422,7 +1410,7 @@ protected function _prepareStaticValue($key, $value) /** * Save object collected data * - * @param array $saveData array('newObject', 'entityRow', 'insert', 'update', 'delete') + * @param array $saveData array('newObject', 'entityRow', 'insert', 'update', 'delete') * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1517,9 +1505,9 @@ protected function _processSaveData($saveData) /** * Insert entity attribute value * - * @param DataObject $object - * @param AbstractAttribute $attribute - * @param mixed $value + * @param DataObject $object + * @param AbstractAttribute $attribute + * @param mixed $value * @return $this */ protected function _insertAttribute($object, $attribute, $value) @@ -1530,10 +1518,10 @@ protected function _insertAttribute($object, $attribute, $value) /** * Update entity attribute value * - * @param DataObject $object - * @param AbstractAttribute $attribute - * @param mixed $valueId - * @param mixed $value + * @param DataObject $object + * @param AbstractAttribute $attribute + * @param mixed $valueId + * @param mixed $value * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -1892,10 +1880,12 @@ protected function _getDefaultAttributes() */ public function getDefaultAttributes() { - return array_unique(array_merge( - $this->_getDefaultAttributes(), - [$this->getEntityIdField(), $this->getLinkField()] - )); + return array_unique( + array_merge( + $this->_getDefaultAttributes(), + [$this->getEntityIdField(), $this->getLinkField()] + ) + ); } /** diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index bb2477d4df827..7d48b0c1a8271 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -32,12 +32,12 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im const ATTRIBUTE_CODE_MAX_LENGTH = 60; /** - * Attribute code min length. + * Min accepted length of an attribute code. */ const ATTRIBUTE_CODE_MIN_LENGTH = 1; /** - * Cache tag + * Tag to use for attributes caching. */ const CACHE_TAG = 'EAV_ATTRIBUTE'; @@ -311,7 +311,7 @@ public function beforeSave() } /** - * Save additional data + * @inheritdoc * * @return $this * @throws LocalizedException diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 3857118ae67ca..7066a752fe2a2 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -329,7 +329,7 @@ public function getAttributeCode() } /** - * Set attribute model + * Set attribute model class. * * @param array $data * @return $this diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/SpecificSourceInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/SpecificSourceInterface.php new file mode 100644 index 0000000000000..c6422962f6b1b --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/SpecificSourceInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Eav\Model\Entity\Attribute\Source; + +use Magento\Framework\Api\CustomAttributesDataInterface; + +/** + * Can provide entity-specific options for an attribute. + */ +interface SpecificSourceInterface extends SourceInterface +{ + /** + * List of options specific to an entity. + * + * Same format as for "getAllOptions". + * Will be called instead of "getAllOptions". + * + * @param CustomAttributesDataInterface $entity + * @return array + */ + public function getOptionsFor(CustomAttributesDataInterface $entity): array; +} diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 0e7a46125d872..9e4ad6fb53a33 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -11,7 +11,9 @@ use Magento\Eav\Model\Entity\Attribute as EntityAttribute; use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; +use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; /** * EAV attribute resource model @@ -20,7 +22,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Attribute extends AbstractDb { /** * Eav Entity attributes cache @@ -189,6 +191,23 @@ protected function _beforeSave(AbstractModel $object) return parent::_beforeSave($object); } + /** + * @inheritdoc + * + * @param AbstractModel $attribute + * @return AbstractDb + * @throws CouldNotDeleteException + */ + protected function _beforeDelete(AbstractModel $attribute) + { + /** @var $attribute \Magento\Eav\Api\Data\AttributeInterface */ + if ($attribute->getId() && !$attribute->getIsUserDefined()) { + throw new CouldNotDeleteException(__("The system attribute can't be deleted.")); + } + + return parent::_beforeDelete($attribute); + } + /** * Save additional attribute data after save attribute * @@ -457,6 +476,7 @@ protected function _updateAttributeOption($object, $optionId, $option) if (!empty($option['delete'][$optionId])) { if ($intOptionId) { $connection->delete($table, ['option_id = ?' => $intOptionId]); + $this->clearSelectedOptionInEntities($object, $intOptionId); } return false; } @@ -475,6 +495,41 @@ protected function _updateAttributeOption($object, $optionId, $option) return $intOptionId; } + /** + * Clear selected option in entities + * + * @param EntityAttribute|AbstractModel $object + * @param int $optionId + * @return void + */ + private function clearSelectedOptionInEntities(AbstractModel $object, int $optionId) + { + $backendTable = $object->getBackendTable(); + $attributeId = $object->getAttributeId(); + if (!$backendTable || !$attributeId) { + return; + } + + $connection = $this->getConnection(); + $where = $connection->quoteInto('attribute_id = ?', $attributeId); + $update = []; + + if ($object->getBackendType() === 'varchar') { + $where.= ' AND ' . $connection->prepareSqlCondition('value', ['finset' => $optionId]); + $concat = $connection->getConcatSql(["','", 'value', "','"]); + $expr = $connection->quoteInto( + "TRIM(BOTH ',' FROM REPLACE($concat,',?,',','))", + $optionId + ); + $update['value'] = new \Zend_Db_Expr($expr); + } else { + $where.= $connection->quoteInto(' AND value = ?', $optionId); + $update['value'] = null; + } + + $connection->update($backendTable, $update, $where); + } + /** * Save option values records per store * diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Option.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Option.php index 6dc51247fb3f3..a7be59e2c05d5 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Option.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Option.php @@ -42,10 +42,13 @@ public function addOptionValueToCollection($collection, $attribute, $valueExpr) "{$optionTable2}.option_id={$valueExpr} AND {$optionTable2}.store_id=?", $collection->getStoreId() ); - $valueExpr = $connection->getCheckSql( - "{$optionTable2}.value_id IS NULL", - "{$optionTable1}.option_id", - "{$optionTable2}.option_id" + $valueIdExpr = $connection->getIfNullSql( + "{$optionTable2}.option_id", + "{$optionTable1}.option_id" + ); + $valueExpr = $connection->getIfNullSql( + "{$optionTable2}.value", + "{$optionTable1}.value" ); $collection->getSelect()->joinLeft( @@ -55,7 +58,10 @@ public function addOptionValueToCollection($collection, $attribute, $valueExpr) )->joinLeft( [$optionTable2 => $this->getTable('eav_attribute_option_value')], $tableJoinCond2, - [$attributeCode => $valueExpr] + [ + $attributeCode => $valueIdExpr, + $attributeCode . '_value' => $valueExpr, + ] ); return $this; diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index cd0d5141154c0..15dcea077c887 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ +namespace Magento\Eav\Model\Validator\Attribute; + +use Magento\Eav\Model\Attribute; + /** * EAV attribute data validator * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Eav\Model\Validator\Attribute; - -use Magento\Eav\Model\Attribute; - class Data extends \Magento\Framework\Validator\AbstractValidator { /** @@ -126,7 +126,7 @@ public function isValid($entity) $dataModel = $this->_attrDataFactory->create($attribute, $entity); $dataModel->setExtractedData($data); if (!isset($data[$attributeCode])) { - $data[$attributeCode] = null; + $data[$attributeCode] = ''; } $result = $dataModel->validateValue($data[$attributeCode]); if (true !== $result) { diff --git a/app/code/Magento/Eav/Setup/AddOptionToAttribute.php b/app/code/Magento/Eav/Setup/AddOptionToAttribute.php new file mode 100644 index 0000000000000..c6b13f8a6e3ec --- /dev/null +++ b/app/code/Magento/Eav/Setup/AddOptionToAttribute.php @@ -0,0 +1,210 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Setup; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Setup\ModuleDataSetupInterface; + +/** + * Add option to attribute + */ +class AddOptionToAttribute +{ + /** + * @var ModuleDataSetupInterface + */ + private $setup; + + /** + * @param ModuleDataSetupInterface $setup + */ + public function __construct( + ModuleDataSetupInterface $setup + ) { + $this->setup = $setup; + } + + /** + * Add Attribute Option + * + * @param array $option + * + * @return void + * @throws LocalizedException + */ + public function execute(array $option): void + { + $optionTable = $this->setup->getTable('eav_attribute_option'); + $optionValueTable = $this->setup->getTable('eav_attribute_option_value'); + + if (isset($option['value'])) { + $this->addValue($option, $optionTable, $optionValueTable); + } elseif (isset($option['values'])) { + $this->addValues($option, $optionTable, $optionValueTable); + } + } + + /** + * Add option value + * + * @param array $option + * @param string $optionTable + * @param string $optionValueTable + * + * @return void + * @throws LocalizedException + */ + private function addValue(array $option, string $optionTable, string $optionValueTable): void + { + $value = $option['value']; + foreach ($value as $optionId => $values) { + $intOptionId = (int)$optionId; + if (!empty($option['delete'][$optionId])) { + if ($intOptionId) { + $condition = ['option_id =?' => $intOptionId]; + $this->setup->getConnection()->delete($optionTable, $condition); + } + continue; + } + + if (!$intOptionId) { + $data = [ + 'attribute_id' => $option['attribute_id'], + 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, + ]; + $this->setup->getConnection()->insert($optionTable, $data); + $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); + } else { + $data = [ + 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, + ]; + $this->setup->getConnection()->update( + $optionTable, + $data, + ['option_id=?' => $intOptionId] + ); + } + + // Default value + if (!isset($values[0])) { + throw new LocalizedException( + __("The default option isn't defined. Set the option and try again.") + ); + } + $condition = ['option_id =?' => $intOptionId]; + $this->setup->getConnection()->delete($optionValueTable, $condition); + foreach ($values as $storeId => $value) { + $data = ['option_id' => $intOptionId, 'store_id' => $storeId, 'value' => $value]; + $this->setup->getConnection()->insert($optionValueTable, $data); + } + } + } + + /** + * Add option values + * + * @param array $option + * @param string $optionTable + * @param string $optionValueTable + * + * @return void + */ + private function addValues(array $option, string $optionTable, string $optionValueTable): void + { + $values = $option['values']; + $attributeId = (int)$option['attribute_id']; + $existingOptions = $this->getExistingAttributeOptions($attributeId, $optionTable, $optionValueTable); + foreach ($values as $sortOrder => $value) { + // add option + $data = ['attribute_id' => $attributeId, 'sort_order' => $sortOrder]; + if (!$this->isExistingOptionValue($value, $existingOptions)) { + $this->setup->getConnection()->insert($optionTable, $data); + + //add option value + $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); + $data = ['option_id' => $intOptionId, 'store_id' => 0, 'value' => $value]; + $this->setup->getConnection()->insert($optionValueTable, $data); + } elseif ($optionId = $this->getExistingOptionIdWithDiffSortOrder( + $sortOrder, + $value, + $existingOptions + ) + ) { + $this->setup->getConnection()->update( + $optionTable, + ['sort_order' => $sortOrder], + ['option_id = ?' => $optionId] + ); + } + } + } + + /** + * Check if option value already exists + * + * @param string $value + * @param array $existingOptions + * + * @return bool + */ + private function isExistingOptionValue(string $value, array $existingOptions): bool + { + foreach ($existingOptions as $option) { + if ($option['value'] == $value) { + return true; + } + } + + return false; + } + + /** + * Get existing attribute options + * + * @param int $attributeId + * @param string $optionTable + * @param string $optionValueTable + * + * @return array + */ + private function getExistingAttributeOptions(int $attributeId, string $optionTable, string $optionValueTable): array + { + $select = $this->setup + ->getConnection() + ->select() + ->from(['o' => $optionTable]) + ->reset('columns') + ->columns(['option_id', 'sort_order']) + ->join(['ov' => $optionValueTable], 'o.option_id = ov.option_id', 'value') + ->where(AttributeInterface::ATTRIBUTE_ID . ' = ?', $attributeId) + ->where('store_id = 0'); + + return $this->setup->getConnection()->fetchAll($select); + } + + /** + * Check if option already exists, but sort_order differs + * + * @param int $sortOrder + * @param string $value + * @param array $existingOptions + * + * @return int|null + */ + private function getExistingOptionIdWithDiffSortOrder(int $sortOrder, string $value, array $existingOptions): ?int + { + foreach ($existingOptions as $option) { + if ($option['value'] == $value && $option['sort_order'] != $sortOrder) { + return (int)$option['option_id']; + } + } + + return null; + } +} diff --git a/app/code/Magento/Eav/Setup/EavSetup.php b/app/code/Magento/Eav/Setup/EavSetup.php index de285e81b1d03..d440a84fc8e65 100644 --- a/app/code/Magento/Eav/Setup/EavSetup.php +++ b/app/code/Magento/Eav/Setup/EavSetup.php @@ -82,6 +82,11 @@ class EavSetup */ private $_defaultAttributeSetName = 'Default'; + /** + * @var AddOptionToAttribute + */ + private $addAttributeOption; + /** * @var Code */ @@ -95,18 +100,23 @@ class EavSetup * @param CacheInterface $cache * @param CollectionFactory $attrGroupCollectionFactory * @param Code|null $attributeCodeValidator + * @param AddOptionToAttribute|null $addAttributeOption + * @SuppressWarnings(PHPMD.LongVariable) */ public function __construct( ModuleDataSetupInterface $setup, Context $context, CacheInterface $cache, CollectionFactory $attrGroupCollectionFactory, - Code $attributeCodeValidator = null + Code $attributeCodeValidator = null, + AddOptionToAttribute $addAttributeOption = null ) { $this->cache = $cache; $this->attrGroupCollectionFactory = $attrGroupCollectionFactory; $this->attributeMapper = $context->getAttributeMapper(); $this->setup = $setup; + $this->addAttributeOption = $addAttributeOption + ?? ObjectManager::getInstance()->get(AddOptionToAttribute::class); $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( Code::class ); @@ -567,6 +577,7 @@ public function addAttributeGroup($entityTypeId, $setId, $name, $sortOrder = nul if (empty($data['attribute_group_code'])) { if (empty($attributeGroupCode)) { // in the following code md5 is not used for security purposes + // phpcs:disable Magento2.Security.InsecureFunction $attributeGroupCode = md5($name); } $data['attribute_group_code'] = $attributeGroupCode; @@ -868,62 +879,7 @@ public function addAttribute($entityTypeId, $code, array $attr) */ public function addAttributeOption($option) { - $optionTable = $this->setup->getTable('eav_attribute_option'); - $optionValueTable = $this->setup->getTable('eav_attribute_option_value'); - - if (isset($option['value'])) { - foreach ($option['value'] as $optionId => $values) { - $intOptionId = (int)$optionId; - if (!empty($option['delete'][$optionId])) { - if ($intOptionId) { - $condition = ['option_id =?' => $intOptionId]; - $this->setup->getConnection()->delete($optionTable, $condition); - } - continue; - } - - if (!$intOptionId) { - $data = [ - 'attribute_id' => $option['attribute_id'], - 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, - ]; - $this->setup->getConnection()->insert($optionTable, $data); - $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); - } else { - $data = [ - 'sort_order' => isset($option['order'][$optionId]) ? $option['order'][$optionId] : 0, - ]; - $this->setup->getConnection()->update( - $optionTable, - $data, - ['option_id=?' => $intOptionId] - ); - } - - // Default value - if (!isset($values[0])) { - throw new \Magento\Framework\Exception\LocalizedException( - __("The default option isn't defined. Set the option and try again.") - ); - } - $condition = ['option_id =?' => $intOptionId]; - $this->setup->getConnection()->delete($optionValueTable, $condition); - foreach ($values as $storeId => $value) { - $data = ['option_id' => $intOptionId, 'store_id' => $storeId, 'value' => $value]; - $this->setup->getConnection()->insert($optionValueTable, $data); - } - } - } elseif (isset($option['values'])) { - foreach ($option['values'] as $sortOrder => $label) { - // add option - $data = ['attribute_id' => $option['attribute_id'], 'sort_order' => $sortOrder]; - $this->setup->getConnection()->insert($optionTable, $data); - $intOptionId = $this->setup->getConnection()->lastInsertId($optionTable); - - $data = ['option_id' => $intOptionId, 'store_id' => 0, 'value' => $label]; - $this->setup->getConnection()->insert($optionValueTable, $data); - } - } + $this->addAttributeOption->execute($option); } /** diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php index 07ce6fbfc6a4c..acba37cc45788 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php @@ -4,13 +4,54 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Unit\Model\Validator\Attribute; + /** * Test for \Magento\Eav\Model\Validator\Attribute\Data */ -namespace Magento\Eav\Test\Unit\Model\Validator\Attribute; - class DataTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Eav\Model\AttributeDataFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $attrDataFactory; + + /** + * @var \Magento\Eav\Model\Validator\Attribute\Data + */ + private $model; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->attrDataFactory = $this->getMockBuilder(\Magento\Eav\Model\AttributeDataFactory::class) + ->setMethods(['create']) + ->setConstructorArgs( + [ + 'objectManager' => $this->createMock(\Magento\Framework\ObjectManagerInterface::class), + 'string' => $this->createMock(\Magento\Framework\Stdlib\StringUtils::class) + ] + ) + ->getMock(); + + $this->model = $this->objectManager->getObject( + \Magento\Eav\Model\Validator\Attribute\Data::class, + [ + '_attrDataFactory' => $this->attrDataFactory + ] + ); + } + /** * Testing \Magento\Eav\Model\Validator\Attribute\Data::isValid * @@ -381,13 +422,15 @@ public function testAddErrorMessages() protected function _getAttributeMock($attributeData) { $attribute = $this->getMockBuilder(\Magento\Eav\Model\Attribute::class) - ->setMethods([ - 'getAttributeCode', - 'getDataModel', - 'getFrontendInput', - '__wakeup', - 'getIsVisible', - ]) + ->setMethods( + [ + 'getAttributeCode', + 'getDataModel', + 'getFrontendInput', + '__wakeup', + 'getIsVisible', + ] + ) ->disableOriginalConstructor() ->getMock(); @@ -436,7 +479,7 @@ protected function _getDataModelMock($returnValue, $argument = null) $dataModel = $this->getMockBuilder( \Magento\Eav\Model\Attribute\Data\AbstractData::class )->disableOriginalConstructor()->setMethods( - ['validateValue'] + ['setExtractedData', 'validateValue'] )->getMockForAbstractClass(); if ($argument) { $dataModel->expects( @@ -466,4 +509,24 @@ protected function _getEntityMock() )->disableOriginalConstructor()->getMock(); return $entity; } + + /** + * Test for isValid() without data for attribute. + * + * @return void + */ + public function testIsValidWithoutData() : void + { + $attributeData = ['attribute_code' => 'attribute', 'frontend_input' => 'text', 'is_visible' => true]; + $entity = $this->_getEntityMock(); + $attribute = $this->_getAttributeMock($attributeData); + $dataModel = $this->_getDataModelMock(true, $this->logicalAnd($this->isEmpty(), $this->isType('string'))); + $dataModel->expects($this->once())->method('setExtractedData')->with([])->willReturnSelf(); + $this->attrDataFactory->expects($this->once()) + ->method('create') + ->with($attribute, $entity) + ->willReturn($dataModel); + $this->model->setAttributes([$attribute])->setData([]); + $this->assertTrue($this->model->isValid($entity)); + } } diff --git a/app/code/Magento/Eav/Test/Unit/Setup/AddAttributeOptionTest.php b/app/code/Magento/Eav/Test/Unit/Setup/AddAttributeOptionTest.php new file mode 100644 index 0000000000000..17376ceebbcb4 --- /dev/null +++ b/app/code/Magento/Eav/Test/Unit/Setup/AddAttributeOptionTest.php @@ -0,0 +1,200 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Unit\Setup; + +use Magento\Eav\Setup\AddOptionToAttribute; +use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Coverage for \Magento\Eav\Setup\AddOptionToAttribute + */ +class AddAttributeOptionTest extends TestCase +{ + /** + * @var AddOptionToAttribute + */ + private $operation; + + /** + * @var MockObject + */ + private $connectionMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + $setupMock = $this->createMock(ModuleDataSetupInterface::class); + $this->connectionMock = $this->createMock(Mysql::class); + $this->connectionMock->method('select') + ->willReturn($objectManager->getObject(Select::class)); + + $setupMock->method('getTable')->willReturn('some_table'); + $setupMock->method('getConnection')->willReturn($this->connectionMock); + + $this->operation = new AddOptionToAttribute($setupMock); + } + + /** + * @throws LocalizedException + */ + public function testAddNewOptions() + { + $this->connectionMock->method('fetchAll')->willReturn([]); + $this->connectionMock->expects($this->exactly(4))->method('insert'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddExistingOptionsWithTheSameSortOrder() + { + $this->connectionMock->method('fetchAll')->willReturn( + [ + ['option_id' => 1, 'sort_order' => 0, 'value' => 'Black'], + ['option_id' => 2, 'sort_order' => 1, 'value' => 'White'], + ] + ); + + $this->connectionMock->expects($this->never())->method('insert'); + $this->connectionMock->expects($this->never())->method('update'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddExistingOptionsWithDifferentSortOrder() + { + $this->connectionMock->method('fetchAll')->willReturn( + [ + ['option_id' => 1, 'sort_order' => 13, 'value' => 'Black'], + ['option_id' => 2, 'sort_order' => 666, 'value' => 'White'], + ] + ); + + $this->connectionMock->expects($this->never())->method('insert'); + $this->connectionMock->expects($this->exactly(2))->method('update'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddMixedOptions() + { + $this->connectionMock->method('fetchAll')->willReturn( + [ + ['option_id' => 1, 'sort_order' => 13, 'value' => 'Black'], + ] + ); + + $this->connectionMock->expects($this->exactly(2))->method('insert'); + $this->connectionMock->expects($this->once())->method('update'); + + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => 4 + ] + ); + } + + /** + * @throws LocalizedException + */ + public function testAddNewOption() + { + $this->connectionMock->expects($this->exactly(2))->method('insert'); + $this->connectionMock->expects($this->once())->method('delete'); + + $this->operation->execute( + [ + 'attribute_id' => 1, + 'order' => [0 => 13], + 'value' => [ + [ + 0 => 'zzz', + ], + ], + ] + ); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The default option isn't defined. Set the option and try again. + */ + public function testAddNewOptionWithoutDefaultValue() + { + $this->operation->execute( + [ + 'attribute_id' => 1, + 'order' => [0 => 13], + 'value' => [[]], + ] + ); + } + + public function testDeleteOption() + { + $this->connectionMock->expects($this->never())->method('insert'); + $this->connectionMock->expects($this->never())->method('update'); + $this->connectionMock->expects($this->once())->method('delete'); + + $this->operation->execute( + [ + 'attribute_id' => 1, + 'delete' => [13 => true], + 'value' => [ + 13 => null, + ], + ] + ); + } + + public function testUpdateOption() + { + $this->connectionMock->expects($this->once())->method('insert'); + $this->connectionMock->expects($this->once())->method('update'); + $this->connectionMock->expects($this->once())->method('delete'); + + $this->operation->execute( + [ + 'attribute_id' => 1, + 'value' => [ + 13 => ['zzz'], + ], + ] + ); + } +} diff --git a/app/code/Magento/Eav/etc/adminhtml/system.xml b/app/code/Magento/Eav/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..86916abe812d9 --- /dev/null +++ b/app/code/Magento/Eav/etc/adminhtml/system.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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="dev"> + <group id="caching" translate="label" type="text" sortOrder="120" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Caching Settings</label> + <field id="cache_user_defined_attributes" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Cache User Defined Attributes</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>By default only system EAV attributes are cached.</comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/Eav/etc/config.xml b/app/code/Magento/Eav/etc/config.xml index 2a86437d96abe..c0a4287909b7d 100644 --- a/app/code/Magento/Eav/etc/config.xml +++ b/app/code/Magento/Eav/etc/config.xml @@ -20,5 +20,10 @@ </input_types> </validator_data> </general> + <dev> + <caching> + <cache_user_defined_attributes>0</cache_user_defined_attributes> + </caching> + </dev> </default> </config> diff --git a/app/code/Magento/Eav/etc/db_schema.xml b/app/code/Magento/Eav/etc/db_schema.xml index b6c42d725e5e9..8dc917b22d09f 100644 --- a/app/code/Magento/Eav/etc/db_schema.xml +++ b/app/code/Magento/Eav/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="eav_entity_type" resource="default" engine="innodb" comment="Eav Entity Type"> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Entity Type Id"/> + comment="Entity Type ID"/> <column xsi:type="varchar" name="entity_type_code" nullable="false" length="50" comment="Entity Type Code"/> <column xsi:type="varchar" name="entity_model" nullable="false" length="255" comment="Entity Model"/> <column xsi:type="varchar" name="attribute_model" nullable="true" length="255" comment="Attribute Model"/> @@ -21,7 +21,7 @@ <column xsi:type="varchar" name="data_sharing_key" nullable="true" length="100" default="default" comment="Data Sharing Key"/> <column xsi:type="smallint" name="default_attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Default Attribute Set Id"/> + identity="false" default="0" comment="Default Attribute Set ID"/> <column xsi:type="varchar" name="increment_model" nullable="true" length="255" comment="Increment Model"/> <column xsi:type="smallint" name="increment_per_store" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Increment Per Store"/> @@ -44,14 +44,14 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + identity="false" default="0" comment="Attribute Set ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Parent Id"/> + default="0" comment="Parent ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -75,13 +75,13 @@ </table> <table name="eav_entity_datetime" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Attribute Value"/> @@ -115,13 +115,13 @@ </table> <table name="eav_entity_decimal" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" @@ -156,13 +156,13 @@ </table> <table name="eav_entity_int" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" @@ -196,13 +196,13 @@ </table> <table name="eav_entity_text" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="false" comment="Attribute Value"/> @@ -233,13 +233,13 @@ </table> <table name="eav_entity_varchar" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Attribute Value"/> @@ -273,9 +273,9 @@ </table> <table name="eav_attribute" resource="default" engine="innodb" comment="Eav Attribute"> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="varchar" name="attribute_code" nullable="false" length="255" comment="Attribute Code"/> <column xsi:type="varchar" name="attribute_model" nullable="true" length="255" comment="Attribute Model"/> <column xsi:type="varchar" name="backend_model" nullable="true" length="255" comment="Backend Model"/> @@ -308,13 +308,13 @@ </table> <table name="eav_entity_store" resource="default" engine="innodb" comment="Eav Entity Store"> <column xsi:type="int" name="entity_store_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Store Id"/> + comment="Entity Store ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="increment_prefix" nullable="true" length="20" comment="Increment Prefix"/> - <column xsi:type="varchar" name="increment_last_id" nullable="true" length="50" comment="Last Incremented Id"/> + <column xsi:type="varchar" name="increment_last_id" nullable="true" length="50" comment="Last Incremented ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_store_id"/> </constraint> @@ -332,9 +332,9 @@ </table> <table name="eav_attribute_set" resource="default" engine="innodb" comment="Eav Attribute Set"> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Attribute Set Id"/> + comment="Attribute Set ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="varchar" name="attribute_set_name" nullable="true" length="255" comment="Attribute Set Name"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> @@ -355,15 +355,15 @@ </table> <table name="eav_attribute_group" resource="default" engine="innodb" comment="Eav Attribute Group"> <column xsi:type="smallint" name="attribute_group_id" padding="5" unsigned="true" nullable="false" - identity="true" comment="Attribute Group Id"/> + identity="true" comment="Attribute Group ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set Id"/> + identity="false" default="0" comment="Attribute Set ID"/> <column xsi:type="varchar" name="attribute_group_name" nullable="true" length="255" comment="Attribute Group Name"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="default_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Default Id"/> + default="0" comment="Default ID"/> <column xsi:type="varchar" name="attribute_group_code" nullable="false" length="255" comment="Attribute Group Code"/> <column xsi:type="varchar" name="tab_group_code" nullable="true" length="255" comment="Tab Group Code"/> @@ -388,15 +388,15 @@ </table> <table name="eav_entity_attribute" resource="default" engine="innodb" comment="Eav Entity Attributes"> <column xsi:type="int" name="entity_attribute_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Attribute Id"/> + comment="Entity Attribute ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set Id"/> + identity="false" default="0" comment="Attribute Set ID"/> <column xsi:type="smallint" name="attribute_group_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Group Id"/> + identity="false" default="0" comment="Attribute Group ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -426,9 +426,9 @@ </table> <table name="eav_attribute_option" resource="default" engine="innodb" comment="Eav Attribute Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -443,11 +443,11 @@ </table> <table name="eav_attribute_option_value" resource="default" engine="innodb" comment="Eav Attribute Option Value"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> @@ -467,11 +467,11 @@ </table> <table name="eav_attribute_label" resource="default" engine="innodb" comment="Eav Attribute Label"> <column xsi:type="int" name="attribute_label_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Attribute Label Id"/> + comment="Attribute Label ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_label_id"/> @@ -481,6 +481,10 @@ referenceColumn="attribute_id" onDelete="CASCADE"/> <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" table="eav_attribute_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_STORE_ID_UNIQUE"> + <column name="store_id"/> + <column name="attribute_id"/> + </constraint> <index referenceId="EAV_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> @@ -491,14 +495,14 @@ </table> <table name="eav_form_type" resource="default" engine="innodb" comment="Eav Form Type"> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="varchar" name="code" nullable="false" length="64" comment="Code"/> <column xsi:type="varchar" name="label" nullable="false" length="255" comment="Label"/> <column xsi:type="smallint" name="is_system" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is System"/> <column xsi:type="varchar" name="theme" nullable="true" length="64" comment="Theme"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type_id"/> </constraint> @@ -515,9 +519,9 @@ </table> <table name="eav_form_type_entity" resource="default" engine="innodb" comment="Eav Form Type Entity"> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Entity Type Id"/> + comment="Entity Type ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type_id"/> <column name="entity_type_id"/> @@ -534,9 +538,9 @@ </table> <table name="eav_form_fieldset" resource="default" engine="innodb" comment="Eav Form Fieldset"> <column xsi:type="smallint" name="fieldset_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Fieldset Id"/> + comment="Fieldset ID"/> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="varchar" name="code" nullable="false" length="64" comment="Code"/> <column xsi:type="int" name="sort_order" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> @@ -552,9 +556,9 @@ </table> <table name="eav_form_fieldset_label" resource="default" engine="innodb" comment="Eav Form Fieldset Label"> <column xsi:type="smallint" name="fieldset_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Fieldset Id"/> + comment="Fieldset ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="label" nullable="false" length="255" comment="Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="fieldset_id"/> @@ -572,13 +576,13 @@ </table> <table name="eav_form_element" resource="default" engine="innodb" comment="Eav Form Element"> <column xsi:type="int" name="element_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Element Id"/> + comment="Element ID"/> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="smallint" name="fieldset_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Fieldset Id"/> + comment="Fieldset ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="int" name="sort_order" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Eav/etc/db_schema_whitelist.json b/app/code/Magento/Eav/etc/db_schema_whitelist.json index b3f1aca50df01..fbcba0741eadf 100644 --- a/app/code/Magento/Eav/etc/db_schema_whitelist.json +++ b/app/code/Magento/Eav/etc/db_schema_whitelist.json @@ -304,7 +304,8 @@ "constraint": { "PRIMARY": true, "EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true, - "EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID": true + "EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID": true, + "EAV_ATTRIBUTE_LABEL_STORE_ID_ATTRIBUTE_ID": true } }, "eav_form_type": { @@ -387,4 +388,4 @@ "EAV_FORM_ELEMENT_TYPE_ID_ATTRIBUTE_ID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index db6f9b0a64f9f..a09dc28399858 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -22,9 +22,9 @@ <preference for="Magento\Eav\Api\AttributeOptionManagementInterface" type="Magento\Eav\Model\Entity\Attribute\OptionManagement" /> <preference for="Magento\Eav\Api\Data\AttributeOptionLabelInterface" type="Magento\Eav\Model\Entity\Attribute\OptionLabel" /> <preference for="Magento\Eav\Api\Data\AttributeValidationRuleInterface" type="Magento\Eav\Model\Entity\Attribute\ValidationRule" /> - <preference for="Magento\Eav\Api\Data\AttributeSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Eav\Api\Data\AttributeSetSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Eav\Api\Data\AttributeGroupSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Eav\Api\Data\AttributeSearchResultsInterface" type="Magento\Eav\Model\AttributeSearchResults" /> + <preference for="Magento\Eav\Api\Data\AttributeSetSearchResultsInterface" type="Magento\Eav\Model\AttributeSetSearchResults" /> + <preference for="Magento\Eav\Api\Data\AttributeGroupSearchResultsInterface" type="Magento\Eav\Model\AttributeGroupSearchResults" /> <preference for="Magento\Framework\Webapi\CustomAttributeTypeLocatorInterface" type="Magento\Eav\Model\TypeLocator" /> <type name="Magento\Eav\Model\Entity\Attribute\Config"> diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php index e4c27adc60247..7361d52372cd6 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php @@ -57,29 +57,31 @@ public function resolve( array $args = null ) : Value { - return $this->valueFactory->create(function () use ($value) { - $entityType = $this->getEntityType($value); - $attributeCode = $this->getAttributeCode($value); + return $this->valueFactory->create( + function () use ($value) { + $entityType = $this->getEntityType($value); + $attributeCode = $this->getAttributeCode($value); - $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode); - return $optionsData; - }); + $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode); + return $optionsData; + } + ); } /** * Get entity type * * @param array $value - * @return int + * @return string * @throws LocalizedException */ - private function getEntityType(array $value): int + private function getEntityType(array $value): string { if (!isset($value['entity_type'])) { throw new LocalizedException(__('"Entity type should be specified')); } - return (int)$value['entity_type']; + return $value['entity_type']; } /** @@ -101,13 +103,13 @@ private function getAttributeCode(array $value): string /** * Get attribute options data * - * @param int $entityType + * @param string $entityType * @param string $attributeCode * @return array * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException */ - private function getAttributeOptionsData(int $entityType, string $attributeCode): array + private function getAttributeOptionsData(string $entityType, string $attributeCode): array { try { $optionsData = $this->attributeOptionsDataProvider->getData($entityType, $attributeCode); diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php index 62e3f01836619..85445580bb1fb 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\EavGraphQl\Model\Resolver\Query\Type; +use Magento\EavGraphQl\Model\Resolver\Query\FrontendType; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; @@ -26,12 +27,19 @@ class CustomAttributeMetadata implements ResolverInterface */ private $type; + /** + * @var FrontendType + */ + private $frontendType; + /** * @param Type $type + * @param FrontendType $frontendType */ - public function __construct(Type $type) + public function __construct(Type $type, FrontendType $frontendType) { $this->type = $type; + $this->frontendType = $frontendType; } /** @@ -52,6 +60,7 @@ public function resolve( continue; } try { + $frontendType = $this->frontendType->getType($attribute['attribute_code'], $attribute['entity_type']); $type = $this->type->getType($attribute['attribute_code'], $attribute['entity_type']); } catch (InputException $exception) { $attributes['items'][] = new GraphQlNoSuchEntityException( @@ -78,7 +87,8 @@ public function resolve( $attributes['items'][] = [ 'attribute_code' => $attribute['attribute_code'], 'entity_type' => $attribute['entity_type'], - 'attribute_type' => ucfirst($type) + 'attribute_type' => ucfirst($type), + 'input_type' => $frontendType ]; } diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php index 900a31c1093ed..3371fbe658c9c 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php @@ -29,11 +29,13 @@ public function __construct( } /** - * @param int $entityType + * Get attribute options data + * + * @param string $entityType * @param string $attributeCode * @return array */ - public function getData(int $entityType, string $attributeCode): array + public function getData(string $entityType, string $attributeCode): array { $options = $this->optionManager->getItems($entityType, $attributeCode); diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php new file mode 100644 index 0000000000000..c76f19e6dfeb4 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Query; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Webapi\ServiceTypeToEntityTypeMap; + +/** + * Get frontend input type for EAV attribute + */ +class FrontendType +{ + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var ServiceTypeToEntityTypeMap + */ + private $serviceTypeMap; + + /** + * @param AttributeRepositoryInterface $attributeRepository + * @param ServiceTypeToEntityTypeMap $serviceTypeMap + */ + public function __construct( + AttributeRepositoryInterface $attributeRepository, + ServiceTypeToEntityTypeMap $serviceTypeMap + ) { + $this->attributeRepository = $attributeRepository; + $this->serviceTypeMap = $serviceTypeMap; + } + + /** + * Return frontend type for attribute + * + * @param string $attributeCode + * @param string $entityType + * @return null|string + */ + public function getType(string $attributeCode, string $entityType): ?string + { + $mappedEntityType = $this->serviceTypeMap->getEntityType($entityType); + if ($mappedEntityType) { + $entityType = $mappedEntityType; + } + try { + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + } catch (NoSuchEntityException $e) { + return null; + } + return $attribute->getFrontendInput(); + } +} diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index 0b174fbc4d84d..21aa7001fab2b 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -13,6 +13,7 @@ type Attribute @doc(description: "Attribute contains the attribute_type of the s attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") entity_type: String @doc(description: "The type of entity that defines the attribute") attribute_type: String @doc(description: "The data type of the attribute") + input_type: String @doc(description: "The frontend input type of the attribute") attribute_options: [AttributeOption] @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributeOptions") @doc(description: "Attribute options list.") } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index 93f4caa10adf9..b9102bc5e00c4 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -108,21 +108,28 @@ public function testConnection() */ private function buildConfig($options = []) { - $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); // @codingStandardsIgnoreStart $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); // @codingStandardsIgnoreEnd if (!$protocol) { $protocol = 'http'; } - if (!empty($options['port'])) { - $host .= ':' . $options['port']; + + $authString = ''; + if (!empty($options['enableAuth']) && (int)$options['enableAuth'] === 1) { + $authString = "{$options['username']}:{$options['password']}@"; } - if (!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) { - $host = sprintf('%s://%s:%s@%s', $protocol, $options['username'], $options['password'], $host); + + $portString = ''; + if (!empty($options['port'])) { + $portString = ':' . $options['port']; } + $host = $protocol . '://' . $authString . $hostname . $portString; + $options['hosts'] = [$host]; + return $options; } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index 97a76de4b995a..fa193d86c03c7 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -193,17 +193,16 @@ public function addDocs(array $documents, $storeId, $mappedIndexerId) */ public function cleanIndex($storeId, $mappedIndexerId) { + // needed to fix bug with double indices in alias because of second reindex in same process + unset($this->preparedIndex[$storeId]); + $this->checkIndex($storeId, $mappedIndexerId, true); $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex); - if ($this->client->isEmptyIndex($indexName)) { - // use existing index if empty - return $this; - } // prepare new index name and increase version $indexPattern = $this->indexNameResolver->getIndexPattern($storeId, $mappedIndexerId); $version = (int)(str_replace($indexPattern, '', $indexName)); - $newIndexName = $indexPattern . ++$version; + $newIndexName = $indexPattern . (++$version); // remove index if already exists if ($this->client->indexExists($newIndexName)) { @@ -354,12 +353,14 @@ protected function prepareIndex($storeId, $indexName, $mappedIndexerId) { $this->indexBuilder->setStoreId($storeId); $settings = $this->indexBuilder->build(); - $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes([ - 'entityType' => $mappedIndexerId, - // Use store id instead of website id from context for save existing fields mapping. - // In future websiteId will be eliminated due to index stored per store - 'websiteId' => $storeId - ]); + $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes( + [ + 'entityType' => $mappedIndexerId, + // Use store id instead of website id from context for save existing fields mapping. + // In future websiteId will be eliminated due to index stored per store + 'websiteId' => $storeId + ] + ); $settings['index']['mapping']['total_fields']['limit'] = $this->getMappingTotalFieldsLimit($allAttributeTypes); $this->client->createIndex($indexName, ['settings' => $settings]); $this->client->addFieldsMapping( diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index 165f7e78eb65f..41a50961ae4bc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -115,6 +115,18 @@ public function isBooleanType(): bool && $this->getAttribute()->getBackendType() !== 'varchar'; } + /** + * Check if attribute is text type + * + * @return bool + */ + public function isTextType(): bool + { + return in_array($this->getAttribute()->getBackendType(), ['varchar', 'static'], true) + && in_array($this->getFrontendInput(), ['text'], true) + && $this->getAttribute()->getIsVisible(); + } + /** * Check if attribute has boolean type. * diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index 6876b23bbb156..348a1c708a78c 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -130,6 +130,21 @@ public function getFields(array $context = []): array ]; } + if ($attributeAdapter->isTextType()) { + $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $index = $this->indexTypeConverter->convert( + IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE + ); + $allAttributes[$fieldName]['fields'][$keywordFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ) + ]; + if ($index) { + $allAttributes[$fieldName]['fields'][$keywordFieldName]['index'] = $index; + } + } + if ($attributeAdapter->isComplexType()) { $childFieldName = $this->fieldNameResolver->getFieldName( $attributeAdapter, diff --git a/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php index f9b827304446d..d933d8bb5d0b5 100644 --- a/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php @@ -103,21 +103,28 @@ public function testConnection() */ private function buildConfig($options = []) { - $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); // @codingStandardsIgnoreStart $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); // @codingStandardsIgnoreEnd if (!$protocol) { $protocol = 'http'; } - if (!empty($options['port'])) { - $host .= ':' . $options['port']; + + $authString = ''; + if (!empty($options['enableAuth']) && (int)$options['enableAuth'] === 1) { + $authString = "{$options['username']}:{$options['password']}@"; } - if (!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) { - $host = sprintf('%s://%s:%s@%s', $protocol, $options['username'], $options['password'], $host); + + $portString = ''; + if (!empty($options['port'])) { + $portString = ':' . $options['port']; } + $host = $protocol . '://' . $authString . $hostname . $portString; + $options['hosts'] = [$host]; + return $options; } diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php index 1c8885fec63be..ce88fc290e23c 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -76,9 +76,6 @@ public function __construct( */ public function resolve(): SearchCriteria { - if ($this->size !== 0) { - $this->builder->setPageSize($this->size); - } $searchCriteria = $this->builder->create(); $searchCriteria->setRequestName($this->searchRequestName); $searchCriteria->setSortOrders($this->orders); diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 3ae2d384782c3..ad52f81bf8eda 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -25,16 +25,32 @@ class SearchResultApplier implements SearchResultApplierInterface */ private $searchResult; + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $currentPage; + /** * @param Collection $collection * @param SearchResultInterface $searchResult + * @param int $size + * @param int $currentPage */ public function __construct( Collection $collection, - SearchResultInterface $searchResult + SearchResultInterface $searchResult, + int $size, + int $currentPage ) { $this->collection = $collection; $this->searchResult = $searchResult; + $this->size = $size; + $this->currentPage = $currentPage; } /** @@ -46,14 +62,36 @@ public function apply() $this->collection->getSelect()->where('NULL'); return; } + + $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); $ids = []; - foreach ($this->searchResult->getItems() as $item) { + foreach ($items as $item) { $ids[] = (int)$item->getId(); } - $this->collection->setPageSize(null); $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); $orderList = join(',', $ids); $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); $this->collection->getSelect()->order("FIELD(e.entity_id,$orderList)"); } + + /** + * Slice current items + * + * @param array $items + * @param int $size + * @param int $currentPage + * @return array + */ + private function sliceItems(array $items, int $size, int $currentPage): array + { + if ($size !== 0) { + $offset = ($currentPage - 1) * $size; + if ($offset < 0) { + $offset = 0; + } + $items = array_slice($items, $offset, $this->size); + } + + return $items; + } } diff --git a/app/code/Magento/Elasticsearch/Observer/CategoryProductIndexer.php b/app/code/Magento/Elasticsearch/Observer/CategoryProductIndexer.php index fd2734bb713b3..e2b3e18a0ffb9 100644 --- a/app/code/Magento/Elasticsearch/Observer/CategoryProductIndexer.php +++ b/app/code/Magento/Elasticsearch/Observer/CategoryProductIndexer.php @@ -11,6 +11,7 @@ use Magento\Elasticsearch\Model\Config; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Catalog\Model\Indexer\Category\Flat\State as FlatState; /** * Checks if a category has changed products and depends on indexer configuration. @@ -27,14 +28,24 @@ class CategoryProductIndexer implements ObserverInterface */ private $processor; + /** + * @var FlatState + */ + private $flatState; + /** * @param Config $config * @param Processor $processor + * @param FlatState $flatState */ - public function __construct(Config $config, Processor $processor) - { + public function __construct( + Config $config, + Processor $processor, + FlatState $flatState + ) { $this->processor = $processor; $this->config = $config; + $this->flatState = $flatState; } /** @@ -47,7 +58,7 @@ public function execute(Observer $observer): void } $productIds = $observer->getEvent()->getProductIds(); - if (!empty($productIds) && $this->processor->isIndexerScheduled()) { + if (!empty($productIds) && $this->processor->isIndexerScheduled() && $this->flatState->isFlatEnabled()) { $this->processor->markIndexerAsInvalid(); } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php index ed8cd049d2915..d88c7e53d813a 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php @@ -5,10 +5,17 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Filter\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Filter\Term as TermFilterRequest; use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +/** + * Term filter builder + */ class Term implements FilterInterface { /** @@ -16,26 +23,56 @@ class Term implements FilterInterface */ protected $fieldMapper; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var array + * @see \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType::$integerTypeAttributes + */ + private $integerTypeAttributes = ['category_ids']; + /** * @param FieldMapperInterface $fieldMapper + * @param AttributeProvider $attributeAdapterProvider + * @param array $integerTypeAttributes */ - public function __construct(FieldMapperInterface $fieldMapper) - { + public function __construct( + FieldMapperInterface $fieldMapper, + AttributeProvider $attributeAdapterProvider = null, + array $integerTypeAttributes = [] + ) { $this->fieldMapper = $fieldMapper; + $this->attributeAdapterProvider = $attributeAdapterProvider + ?? ObjectManager::getInstance()->get(AttributeProvider::class); + $this->integerTypeAttributes = array_merge($this->integerTypeAttributes, $integerTypeAttributes); } /** + * Build term filter request + * * @param RequestFilterInterface|TermFilterRequest $filter * @return array */ public function buildFilter(RequestFilterInterface $filter) { $filterQuery = []; + + $attribute = $this->attributeAdapterProvider->getByAttributeCode($filter->getField()); + $fieldName = $this->fieldMapper->getFieldName($filter->getField()); + + if ($attribute->isTextType() && !in_array($attribute->getAttributeCode(), $this->integerTypeAttributes)) { + $suffix = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $fieldName .= '.' . $suffix; + } + if ($filter->getValue()) { $operator = is_array($filter->getValue()) ? 'terms' : 'term'; $filterQuery []= [ $operator => [ - $this->fieldMapper->getFieldName($filter->getField()) => $filter->getValue(), + $fieldName => $filter->getValue(), ], ]; } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php index afd383c13421f..ddf75c0a78e25 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -138,7 +138,12 @@ protected function buildQueries(array $matches, array $queryValue) $transformedTypes = []; foreach ($matches as $match) { - $attributeAdapter = $this->attributeProvider->getByAttributeCode($match['field']); + $resolvedField = $this->fieldMapper->getFieldName( + $match['field'], + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + + $attributeAdapter = $this->attributeProvider->getByAttributeCode($resolvedField); $fieldType = $this->fieldTypeResolver->getFieldType($attributeAdapter); $valueTransformer = $this->valueTransformerPool->get($fieldType ?? 'text'); $valueTransformerHash = \spl_object_hash($valueTransformer); @@ -151,10 +156,6 @@ protected function buildQueries(array $matches, array $queryValue) continue; } - $resolvedField = $this->fieldMapper->getFieldName( - $match['field'], - ['type' => FieldMapperInterface::TYPE_QUERY] - ); $conditions[] = [ 'condition' => $queryValue['condition'], 'body' => [ diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 5f5807e212961..99fd416b5cd3e 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Client; +use Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch; use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -38,7 +40,7 @@ class ElasticsearchTest extends \PHPUnit\Framework\TestCase * * @return void */ - protected function setUp() + protected function setUp(): void { $this->elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class) ->setMethods( @@ -497,6 +499,40 @@ public function testDeleteMapping() ); } + /** + * Ensure that configuration returns correct url. + * + * @param array $options + * @param string $expectedResult + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \ReflectionException + * @dataProvider getOptionsDataProvider + */ + public function testBuildConfig(array $options, $expectedResult): void + { + $buildConfig = new Elasticsearch($options); + $config = $this->getPrivateMethod(Elasticsearch::class, 'buildConfig'); + $result = $config->invoke($buildConfig, $options); + $this->assertEquals($expectedResult, $result['hosts'][0]); + } + + /** + * Return private method for elastic search class. + * + * @param $className + * @param $methodName + * @return \ReflectionMethod + * @throws \ReflectionException + */ + private function getPrivateMethod($className, $methodName) + { + $reflector = new \ReflectionClass($className); + $method = $reflector->getMethod($methodName); + $method->setAccessible(true); + + return $method; + } + /** * Test deleteMapping() method * @expectedException \Exception @@ -545,6 +581,35 @@ public function testSuggest() $this->assertEquals([], $this->model->suggest($query)); } + /** + * Get options data provider. + */ + public function getOptionsDataProvider() + { + return [ + [ + 'without_protocol' => [ + 'hostname' => 'localhost', + 'port' => '9200', + 'timeout' => 15, + 'index' => 'magento2', + 'enableAuth' => 0, + ], + 'expected_result' => 'http://localhost:9200' + ], + [ + 'with_protocol' => [ + 'hostname' => 'https://localhost', + 'port' => '9200', + 'timeout' => 15, + 'index' => 'magento2', + 'enableAuth' => 0, + ], + 'expected_result' => 'https://localhost:9200' + ] + ]; + } + /** * Get elasticsearch client options * diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php index ec50ba7b3d5df..326c04aad6165 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -77,6 +77,7 @@ class ElasticsearchTest extends \PHPUnit\Framework\TestCase * Setup * * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { @@ -93,10 +94,12 @@ protected function setUp() ->getMock(); $this->clientConfig = $this->getMockBuilder(\Magento\Elasticsearch\Model\Config::class) ->disableOriginalConstructor() - ->setMethods([ - 'getIndexPrefix', - 'getEntityType', - ])->getMock(); + ->setMethods( + [ + 'getIndexPrefix', + 'getEntityType', + ] + )->getMock(); $this->indexBuilder = $this->getMockBuilder(\Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -104,44 +107,52 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class) - ->setMethods([ - 'indices', - 'ping', - 'bulk', - 'search', - ]) + ->setMethods( + [ + 'indices', + 'ping', + 'bulk', + 'search', + ] + ) ->disableOriginalConstructor() ->getMock(); $indicesMock = $this->getMockBuilder(\Elasticsearch\Namespaces\IndicesNamespace::class) - ->setMethods([ - 'exists', - 'getSettings', - 'create', - 'putMapping', - 'deleteMapping', - 'existsAlias', - 'updateAliases', - 'stats' - ]) + ->setMethods( + [ + 'exists', + 'getSettings', + 'create', + 'putMapping', + 'deleteMapping', + 'existsAlias', + 'updateAliases', + 'stats' + ] + ) ->disableOriginalConstructor() ->getMock(); $elasticsearchClientMock->expects($this->any()) ->method('indices') ->willReturn($indicesMock); $this->client = $this->getMockBuilder(\Magento\Elasticsearch\Model\Client\Elasticsearch::class) - ->setConstructorArgs([ - 'options' => $this->getClientOptions(), - 'elasticsearchClient' => $elasticsearchClientMock - ]) + ->setConstructorArgs( + [ + 'options' => $this->getClientOptions(), + 'elasticsearchClient' => $elasticsearchClientMock + ] + ) ->getMock(); $this->connectionManager->expects($this->any()) ->method('getConnection') ->willReturn($this->client); $this->fieldMapper->expects($this->any()) ->method('getAllAttributesTypes') - ->willReturn([ - 'name' => 'string', - ]); + ->willReturn( + [ + 'name' => 'string', + ] + ); $this->clientConfig->expects($this->any()) ->method('getIndexPrefix') ->willReturn('indexName'); @@ -151,12 +162,14 @@ protected function setUp() $this->indexNameResolver = $this->getMockBuilder( \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver::class ) - ->setMethods([ - 'getIndexName', - 'getIndexNamespace', - 'getIndexFromAlias', - 'getIndexNameForAlias', - ]) + ->setMethods( + [ + 'getIndexName', + 'getIndexNamespace', + 'getIndexFromAlias', + 'getIndexNameForAlias', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->batchDocumentDataMapper = $this->getMockBuilder( @@ -216,9 +229,11 @@ public function testPrepareDocsPerStore() { $this->batchDocumentDataMapper->expects($this->once()) ->method('map') - ->willReturn([ - 'name' => 'Product Name', - ]); + ->willReturn( + [ + 'name' => 'Product Name', + ] + ); $this->assertInternalType( 'array', $this->model->prepareDocsPerStore( @@ -283,10 +298,6 @@ public function testCleanIndex() ->with(1, 'product', []) ->willReturn('indexName_product_1_v'); - $this->client->expects($this->once()) - ->method('isEmptyIndex') - ->with('indexName_product_1_v') - ->willReturn(false); $this->client->expects($this->atLeastOnce()) ->method('indexExists') ->willReturn(true); @@ -299,26 +310,6 @@ public function testCleanIndex() ); } - /** - * Test cleanIndex() method isEmptyIndex is true - */ - public function testCleanIndexTrue() - { - $this->indexNameResolver->expects($this->any()) - ->method('getIndexName') - ->willReturn('indexName_product_1_v'); - - $this->client->expects($this->once()) - ->method('isEmptyIndex') - ->with('indexName_product_1_v') - ->willReturn(true); - - $this->assertSame( - $this->model, - $this->model->cleanIndex(1, 'product') - ); - } - /** * Test deleteDocs() method */ @@ -376,9 +367,11 @@ public function testConnectException() { $connectionManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class) ->disableOriginalConstructor() - ->setMethods([ - 'getConnection', - ]) + ->setMethods( + [ + 'getConnection', + ] + ) ->getMock(); $connectionManager->expects($this->any()) diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php index de85b8b6602b8..c08a4298b42d2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -139,6 +139,7 @@ public function testGetAllAttributesTypes( $isComplexType, $complexType, $isSortable, + $isTextType, $fieldName, $compositeFieldName, $sortFieldName, @@ -153,29 +154,33 @@ public function testGetAllAttributesTypes( $this->indexTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) { - if ($type === 'no_index') { - return 'no'; - } elseif ($type === 'no_analyze') { - return 'not_analyzed'; + ->will( + $this->returnCallback( + function ($type) { + if ($type === 'no_index') { + return 'no'; + } elseif ($type === 'no_analyze') { + return 'not_analyzed'; + } } - } - )); + ) + ); $this->fieldNameResolver->expects($this->any()) ->method('getFieldName') ->with($this->anything()) - ->will($this->returnCallback( - function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { - if (empty($context)) { - return $fieldName; - } elseif ($context['type'] === 'sort') { - return $sortFieldName; - } elseif ($context['type'] === 'text') { - return $compositeFieldName; + ->will( + $this->returnCallback( + function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return $sortFieldName; + } elseif ($context['type'] === 'text') { + return $compositeFieldName; + } } - } - )); + ) + ); $productAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode']) @@ -189,7 +194,7 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable']) + ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable', 'isTextType']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') @@ -197,6 +202,9 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $attributeMock->expects($this->any()) ->method('isSortable') ->willReturn($isSortable); + $attributeMock->expects($this->any()) + ->method('isTextType') + ->willReturn($isTextType); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn($attributeCode); @@ -207,22 +215,24 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) use ($complexType) { - static $callCount = []; - $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; + ->will( + $this->returnCallback( + function ($type) use ($complexType) { + static $callCount = []; + $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; - } elseif ($type === 'float') { - return 'float'; - } elseif ($type === 'keyword') { - return 'string'; - } else { - return $complexType; + if ($type === 'string') { + return 'string'; + } elseif ($type === 'float') { + return 'float'; + } elseif ($type === 'keyword') { + return 'string'; + } else { + return $complexType; + } } - } - )); + ) + ); $this->assertEquals( $expected, @@ -243,13 +253,20 @@ public function attributeProvider() true, 'text', false, + true, 'category_ids', 'category_ids_value', '', [ 'category_ids' => [ 'type' => 'select', - 'index' => true + 'index' => true, + 'fields' => [ + 'keyword' => [ + 'type' => 'string', + 'index' => 'not_analyzed' + ] + ] ], 'category_ids_value' => [ 'type' => 'string' @@ -267,13 +284,20 @@ public function attributeProvider() false, null, false, + true, 'attr_code', '', '', [ 'attr_code' => [ 'type' => 'text', - 'index' => 'no' + 'index' => 'no', + 'fields' => [ + 'keyword' => [ + 'type' => 'string', + 'index' => 'not_analyzed' + ] + ] ], 'store_id' => [ 'type' => 'string', @@ -288,6 +312,7 @@ public function attributeProvider() false, null, false, + false, 'attr_code', '', '', @@ -308,6 +333,7 @@ public function attributeProvider() false, null, true, + false, 'attr_code', '', 'sort_attr_code', diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php index fbf63630464f9..b2c0f5e341fc2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php @@ -106,7 +106,7 @@ public function resolveSortOrderDataProvider() ], [ ['size' => 10, 'orders' => ['test' => 'ASC']], - ['size' => 10, 'orders' => ['test' => 'ASC']], + ['size' => null, 'orders' => ['test' => 'ASC']], ], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php index adebee0d591ab..944699908b984 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php @@ -69,7 +69,6 @@ public function testExecuteIfCategoryHasChangedProducts() { $this->getProductIdsWithEnabledElasticSearch(); $this->processorMock->expects($this->once())->method('isIndexerScheduled')->willReturn(true); - $this->processorMock->expects($this->once())->method('markIndexerAsInvalid'); $this->observer->execute($this->observerMock); } diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index 05d3ce94f02f9..3cf5444d86223 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1" + "elasticsearch/elasticsearch": "~2.0||~5.1||~6.1" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 55df6a5a37f46..f42d957276d76 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -267,7 +267,7 @@ </type> <virtualType name="Magento\Elasticsearch\Elasticsearch5\SearchAdapter\ConnectionManager" type="Magento\Elasticsearch\SearchAdapter\ConnectionManager"> <arguments> - <argument name="clientFactory" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory</argument> + <argument name="clientFactory" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy</argument> <argument name="clientConfig" xsi:type="object">Magento\Elasticsearch\Model\Config</argument> </arguments> </virtualType> diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php index 34129a5af0012..e4018196c845d 100644 --- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -103,21 +103,28 @@ public function testConnection() */ private function buildConfig($options = []) { - $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); // @codingStandardsIgnoreStart $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); // @codingStandardsIgnoreEnd if (!$protocol) { $protocol = 'http'; } - if (!empty($options['port'])) { - $host .= ':' . $options['port']; + + $authString = ''; + if (!empty($options['enableAuth']) && (int)$options['enableAuth'] === 1) { + $authString = "{$options['username']}:{$options['password']}@"; } - if (!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) { - $host = sprintf('%s://%s:%s@%s', $protocol, $options['username'], $options['password'], $host); + + $portString = ''; + if (!empty($options['port'])) { + $portString = ':' . $options['port']; } + $host = $protocol . '://' . $authString . $hostname . $portString; + $options['hosts'] = [$host]; + return $options; } diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/ActionGroup/AdminElasticConnectionTestActionGroup.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/ActionGroup/AdminElasticConnectionTestActionGroup.xml new file mode 100644 index 0000000000000..d00c1c59a0f8d --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/ActionGroup/AdminElasticConnectionTestActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminElasticConnectionTestActionGroup"> + <annotations> + <description>Check ElasticSearch connection after enabling.</description> + </annotations> + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="openAdminCatalogSearchConfigPage"/> + <waitForPageLoad stepKey="waitPageToLoad"/> + <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.elastic6ConnectionWizard}}" visible="false" stepKey="expandCatalogSearchTab"/> + <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.elastic6ConnectionWizard}}" stepKey="waitForConnectionButton"/> + <click selector="{{AdminCatalogSearchConfigurationSection.elastic6ConnectionWizard}}" stepKey="clickOnTestConnectionButton"/> + <waitForPageLoad stepKey="waitForConnectionEstablishment"/> + <grabTextFrom selector="{{AdminCatalogSearchConfigurationSection.connectionStatus}}" stepKey="grabConnectionStatus"/> + <assertEquals expected="{{AdminElasticsearch6TestConnectionMessageData.successMessage}}" expectedType="string" actual="$grabConnectionStatus" stepKey="assertThatConnectionSuccessful"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Data/AdminElasticSearch6MessagesData.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/AdminElasticSearch6MessagesData.xml new file mode 100644 index 0000000000000..3fa0ef9fd1c28 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/AdminElasticSearch6MessagesData.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="AdminElasticsearch6TestConnectionMessageData"> + <data key="successMessage">Successful! Test again?</data> + </entity> +</entities> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..f1f2f39f4457b --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,16 @@ +<?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="SearchEngineElasticsearchConfigData"> + <data key="path">catalog/search/engine</data> + <data key="scope_id">1</data> + <data key="label">Elasticsearch 6.0+</data> + <data key="value">elasticsearch6</data> + </entity> +</entities> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Data/Elasticsearch6ConfigData.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/Elasticsearch6ConfigData.xml new file mode 100644 index 0000000000000..7a61f13e62049 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/Elasticsearch6ConfigData.xml @@ -0,0 +1,15 @@ +<?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="EnableElasticSearch6Config"> + <data key="path">catalog/search/engine</data> + <data key="value">elasticsearch6</data> + </entity> +</entities> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Section/AdminCatalogSearchConfigurationSection.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Section/AdminCatalogSearchConfigurationSection.xml new file mode 100644 index 0000000000000..a6f35606ed79b --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Section/AdminCatalogSearchConfigurationSection.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="AdminCatalogSearchConfigurationSection"> + <element name="elastic6ConnectionWizard" type="button" selector="#catalog_search_elasticsearch6_test_connect_wizard"/> + <element name="connectionStatus" type="text" selector="#catalog_search_elasticsearch6_test_connect_wizard_result"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml new file mode 100644 index 0000000000000..d612f5bd17a2f --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="SearchEngineElasticsearchSuite"> + <before> + <magentoCLI stepKey="setSearchEngineToElasticsearch" command="config:set {{SearchEngineElasticsearchConfigData.path}} {{SearchEngineElasticsearchConfigData.value}}"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after></after> + <include> + <group name="SearchEngineElasticsearch" /> + </include> + <exclude> + <group name="skip"/> + </exclude> + </suite> +</suites> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml new file mode 100644 index 0000000000000..fd18a0f4e1e5e --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.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="StorefrontElasticSearch6ForChineseLocaleTest"> + <annotations> + <features value="Elasticsearch6"/> + <stories value="Elasticsearch6 for Chinese"/> + <title value="Elastic search for Chinese locale"/> + <description value="Elastic search for Chinese locale"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-6310"/> + <useCaseId value="MAGETWO-91625"/> + <group value="elasticsearch"/> + </annotations> + <before> + <!-- Set search engine to Elastic 6, set Locale to China, create category and product, then go to Storefront --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <magentoCLI command="config:set --scope={{GeneralLocalCodeConfigsForChina.scope}} --scope-code={{GeneralLocalCodeConfigsForChina.scope_code}} {{GeneralLocalCodeConfigsForChina.path}} {{GeneralLocalCodeConfigsForChina.value}}" stepKey="setLocaleToChina"/> + <magentoCLI command="config:set {{EnableElasticSearch6Config.path}} {{EnableElasticSearch6Config.value}}" stepKey="enableElasticsearch6"/> + <actionGroup ref="AdminElasticConnectionTestActionGroup" stepKey="checkConnection"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + </before> + <after> + <!-- Delete created data and reset initial configuration --> + <magentoCLI command="config:set --scope={{GeneralLocalCodeConfigsForUS.scope}} --scope-code={{GeneralLocalCodeConfigsForUS.scope_code}} {{GeneralLocalCodeConfigsForUS.path}} {{GeneralLocalCodeConfigsForUS.value}}" stepKey="setLocaleToUS"/> + <magentoCLI command="config:set {{SetDefaultSearchEngineConfig.path}} {{SetDefaultSearchEngineConfig.value}}" stepKey="resetSearchEnginePreviousState"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Search for product by name --> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductName"> + <argument name="phrase" value="$$createProduct.name$$"/> + </actionGroup> + <!-- Check if searched product is displayed --> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearch6SearchInvalidValueTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearch6SearchInvalidValueTest.xml new file mode 100644 index 0000000000000..a144a4849db60 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearch6SearchInvalidValueTest.xml @@ -0,0 +1,113 @@ +<?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="StorefrontElasticsearch6SearchInvalidValueTest"> + <annotations> + <features value="Elasticsearch6"/> + <stories value="Search Product on Storefront"/> + <title value="Elasticsearch: try to search by invalid value of 'Searchable' attribute"/> + <description value="Elasticsearch: try to search by invalid value of 'Searchable' attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17906"/> + <useCaseId value="MC-15759"/> + <group value="elasticsearch"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Set Minimal Query Length--> + <magentoCLI command="config:set {{SetMinQueryLength2Config.path}} {{SetMinQueryLength2Config.value}}" stepKey="setMinQueryLength"/> + <!--Reindex indexes and clear cache--> + <magentoCLI command="indexer:reindex catalogsearch_fulltext" stepKey="reindex"/> + <magentoCLI command="cache:flush config" stepKey="flushCache"/> + </before> + <after> + <!--Set configs to default--> + <magentoCLI command="config:set {{SetMinQueryLength3Config.path}} {{SetMinQueryLength3Config.value}}" stepKey="setMinQueryLengthPreviousState"/> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAttributePageLoad"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteProduct"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetFiltersIfExist"/> + <magentoCLI command="indexer:reindex catalogsearch_fulltext" stepKey="reindex"/> + <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Create new searchable product attribute--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminCreateSearchableProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="textProductAttribute"/> + </actionGroup> + <!--Assign attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + <!--Create product and fill new attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> + <argument name="product" value="ProductWithSpecialSymbols"/> + </actionGroup> + <actionGroup ref="SetCategoryByName" stepKey="addCategoryToProduct"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.attributeRequiredInput(textProductAttribute.attribute_code)}}" userInput="searchable" stepKey="fillTheAttributeRequiredInputField"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <!-- TODO: REMOVE AFTER FIX MC-21717 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush eav" stepKey="flushCache"/> + <!--Assert search results on storefront--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm"> + <argument name="phrase" value="?searchable;"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductName"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchProductForSecondSearchTerm"> + <argument name="phrase" value="? searchable ;"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductNameSecondTime"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForSecondSearchTerm"> + <argument name="phrase" value="?;"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForSecondSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchProductForWithSpecialSymbols"> + <argument name="phrase" value="?{{ProductWithSpecialSymbols.name}};"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductWithSpecialSymbols"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchProductForWithSpecialSymbolsSecondTime"> + <argument name="phrase" value="? {{ProductWithSpecialSymbols.name}} ;"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductWithSpecialSymbolsSecondTime"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForThirdSearchTerm"> + <argument name="phrase" value="?anythingcangobetween;"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForThirdSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForForthSearchTerm"> + <argument name="phrase" value="? anything at all ;"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForForthSearchTerm"/> + </test> +</tests> diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php index 487a5a886f951..3ed6721821164 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch6\Test\Unit\Model\Client; use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch6\Model\Client\Elasticsearch; /** * Class ElasticsearchTest @@ -83,7 +85,7 @@ protected function setUp() $this->objectManager = new ObjectManagerHelper($this); $this->model = $this->objectManager->getObject( - \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + Elasticsearch::class, [ 'options' => $this->getOptions(), 'elasticsearchClient' => $this->elasticsearchClientMock @@ -97,7 +99,7 @@ protected function setUp() public function testConstructorOptionsException() { $result = $this->objectManager->getObject( - \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + Elasticsearch::class, [ 'options' => [] ] @@ -119,6 +121,69 @@ public function testConstructorWithOptions() $this->assertNotNull($result); } + /** + * Ensure that configuration returns correct url. + * + * @param array $options + * @param string $expectedResult + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \ReflectionException + * @dataProvider getOptionsDataProvider + */ + public function testBuildConfig(array $options, $expectedResult): void + { + $buildConfig = new Elasticsearch($options); + $config = $this->getPrivateMethod(Elasticsearch::class, 'buildConfig'); + $result = $config->invoke($buildConfig, $options); + $this->assertEquals($expectedResult, $result['hosts'][0]); + } + + /** + * Return private method for elastic search class. + * + * @param $className + * @param $methodName + * @return \ReflectionMethod + * @throws \ReflectionException + */ + private function getPrivateMethod($className, $methodName) + { + $reflector = new \ReflectionClass($className); + $method = $reflector->getMethod($methodName); + $method->setAccessible(true); + + return $method; + } + + /** + * Get options data provider. + */ + public function getOptionsDataProvider() + { + return [ + [ + 'without_protocol' => [ + 'hostname' => 'localhost', + 'port' => '9200', + 'timeout' => 15, + 'index' => 'magento2', + 'enableAuth' => 0, + ], + 'expected_result' => 'http://localhost:9200' + ], + [ + 'with_protocol' => [ + 'hostname' => 'https://localhost', + 'port' => '9200', + 'timeout' => 15, + 'index' => 'magento2', + 'enableAuth' => 0, + ], + 'expected_result' => 'https://localhost:9200' + ] + ]; + } + /** * Test ping functionality */ diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json index 9107f5e900415..b2411f6740fae 100644 --- a/app/code/Magento/Elasticsearch6/composer.json +++ b/app/code/Magento/Elasticsearch6/composer.json @@ -9,7 +9,7 @@ "magento/module-search": "*", "magento/module-store": "*", "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1" + "elasticsearch/elasticsearch": "~2.0||~5.1||~6.1" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch6/etc/di.xml b/app/code/Magento/Elasticsearch6/etc/di.xml index 011dfa1019738..580c61ffc8cdb 100644 --- a/app/code/Magento/Elasticsearch6/etc/di.xml +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -202,4 +202,23 @@ </argument> </arguments> </virtualType> + + <type name="Magento\Config\Model\Config\TypePool"> + <arguments> + <argument name="sensitive" xsi:type="array"> + <item name="catalog/search/elasticsearch6_password" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_server_hostname" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_username" xsi:type="string">1</item> + </argument> + <argument name="environment" xsi:type="array"> + <item name="catalog/search/elasticsearch6_enable_auth" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_index_prefix" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_password" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_server_hostname" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_server_port" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_username" xsi:type="string">1</item> + <item name="catalog/search/elasticsearch6_server_timeout" xsi:type="string">1</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php b/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php index 0ca6615b075b2..ec5596e2194a1 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php @@ -3,12 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** - * Adminhtml system template preview block - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Email\Block\Adminhtml\Template; /** @@ -55,19 +51,22 @@ public function __construct( * Prepare html output * * @return string + * @throws \Exception */ protected function _toHtml() { + $request = $this->getRequest(); + $storeId = $this->getAnyStoreView()->getId(); /** @var $template \Magento\Email\Model\Template */ $template = $this->_emailFactory->create(); - if ($id = (int)$this->getRequest()->getParam('id')) { + if ($id = (int)$request->getParam('id')) { $template->load($id); } else { - $template->setTemplateType($this->getRequest()->getParam('type')); - $template->setTemplateText($this->getRequest()->getParam('text')); - $template->setTemplateStyles($this->getRequest()->getParam('styles')); + $template->setTemplateType($request->getParam('type')); + $template->setTemplateText($request->getParam('text')); + $template->setTemplateStyles($request->getParam('styles')); } \Magento\Framework\Profiler::start($this->profilerName); diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php index 31d172935da7f..4f36eedd09b83 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php @@ -7,12 +7,12 @@ namespace Magento\Email\Controller\Adminhtml\Email\Template; -use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Rendering popup email template. */ -class Popup extends \Magento\Backend\App\Action implements HttpGetActionInterface +class Popup extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php index c1a8eec07e461..a92836b2995a2 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php @@ -4,19 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Email\Controller\Adminhtml\Email\Template; +use Magento\Email\Controller\Adminhtml\Email\Template; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Rendering email template preview. */ -class Preview extends \Magento\Email\Controller\Adminhtml\Email\Template implements HttpGetActionInterface +class Preview extends Template implements HttpGetActionInterface, HttpPostActionInterface { /** * Preview transactional email action. - * - * @return void */ public function execute() { @@ -24,7 +26,6 @@ public function execute() $this->_view->loadLayout(); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Email Preview')); $this->_view->renderLayout(); - $this->getResponse()->setHeader('Content-Security-Policy', "script-src 'self'"); } catch (\Exception $e) { $this->messageManager->addErrorMessage( __('An error occurred. The email template can not be opened for preview.') diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index 5eae1d1462184..8acb0a9fddb75 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -362,12 +362,19 @@ public function getProcessedTemplate(array $variables = []) $variables = $this->addEmailVariables($variables, $storeId); $processor->setVariables($variables); + $previousStrictMode = $processor->setStrictMode( + !$this->getData('is_legacy') && is_numeric($this->getTemplateId()) + ); + try { $result = $processor->filter($this->getTemplateText()); } catch (\Exception $e) { $this->cancelDesignConfig(); throw new \LogicException(__($e->getMessage()), $e->getCode(), $e); + } finally { + $processor->setStrictMode($previousStrictMode); } + if ($isDesignApplied) { $this->cancelDesignConfig(); } @@ -455,6 +462,9 @@ protected function addEmailVariables($variables, $storeId) if (!isset($variables['store'])) { $variables['store'] = $store; } + if (!isset($variables['store']['frontend_name'])) { + $variables['store']['frontend_name'] = $store->getFrontendName(); + } if (!isset($variables['logo_url'])) { $variables['logo_url'] = $this->getLogoUrl($storeId); } diff --git a/app/code/Magento/Email/Model/Template.php b/app/code/Magento/Email/Model/Template.php index 7306db1222872..ca962b31e63af 100644 --- a/app/code/Magento/Email/Model/Template.php +++ b/app/code/Magento/Email/Model/Template.php @@ -246,12 +246,20 @@ public function getProcessedTemplateSubject(array $variables) $this->applyDesignConfig(); $storeId = $this->getDesignConfig()->getStore(); + + $previousStrictMode = $processor->setStrictMode( + !$this->getData('is_legacy') && is_numeric($this->getTemplateId()) + ); + try { $processedResult = $processor->setStoreId($storeId)->filter(__($this->getTemplateSubject())); } catch (\Exception $e) { $this->cancelDesignConfig(); throw new \Magento\Framework\Exception\MailException(__($e->getMessage()), $e); + } finally { + $processor->setStrictMode($previousStrictMode); } + $this->cancelDesignConfig(); return $processedResult; } diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index a29b1165d83c8..82ebc8c78d8b2 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -8,6 +8,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filter\VariableResolverInterface; use Magento\Framework\View\Asset\ContentProcessorException; use Magento\Framework\View\Asset\ContentProcessorInterface; @@ -50,6 +51,7 @@ class Filter extends \Magento\Framework\Filter\Template * Modifier Callbacks * * @var array + * @deprecated Use the new Directive Processor interfaces */ protected $_modifiers = ['nl2br' => '']; @@ -165,15 +167,20 @@ class Filter extends \Magento\Framework\Filter\Template protected $configVariables; /** - * @var \Magento\Email\Model\Template\Css\Processor + * @var Css\Processor */ private $cssProcessor; /** - * @var ReadInterface + * @var Filesystem */ private $pubDirectory; + /** + * @var \Magento\Framework\Filesystem\Directory\Read + */ + private $pubDirectoryRead; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Psr\Log\LoggerInterface $logger @@ -190,7 +197,10 @@ class Filter extends \Magento\Framework\Filter\Template * @param \Magento\Variable\Model\Source\Variables $configVariables * @param array $variables * @param \Magento\Framework\Css\PreProcessor\Adapter\CssInliner|null $cssInliner - * + * @param array $directiveProcessors + * @param VariableResolverInterface|null $variableResolver + * @param Css\Processor|null $cssProcessor + * @param Filesystem|null $pubDirectory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -208,7 +218,11 @@ public function __construct( \Pelago\Emogrifier $emogrifier, \Magento\Variable\Model\Source\Variables $configVariables, $variables = [], - \Magento\Framework\Css\PreProcessor\Adapter\CssInliner $cssInliner = null + \Magento\Framework\Css\PreProcessor\Adapter\CssInliner $cssInliner = null, + array $directiveProcessors = [], + VariableResolverInterface $variableResolver = null, + Css\Processor $cssProcessor = null, + Filesystem $pubDirectory = null ) { $this->_escaper = $escaper; $this->_assetRepo = $assetRepo; @@ -224,8 +238,12 @@ public function __construct( $this->emogrifier = $emogrifier; $this->cssInliner = $cssInliner ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Css\PreProcessor\Adapter\CssInliner::class); + $this->cssProcessor = $cssProcessor ?: ObjectManager::getInstance() + ->get(Css\Processor::class); + $this->pubDirectory = $pubDirectory ?: ObjectManager::getInstance() + ->get(Filesystem::class); $this->configVariables = $configVariables; - parent::__construct($string, $variables); + parent::__construct($string, $variables, $directiveProcessors, $variableResolver); } /** @@ -321,32 +339,14 @@ public function setDesignParams(array $designParams) } /** - * Get CSS processor - * - * @deprecated 100.1.2 - * @return Css\Processor - */ - private function getCssProcessor() - { - if (!$this->cssProcessor) { - $this->cssProcessor = ObjectManager::getInstance()->get(Css\Processor::class); - } - return $this->cssProcessor; - } - - /** - * Get pub directory + * Sets pub directory * - * @deprecated 100.1.2 * @param string $dirType - * @return ReadInterface + * @return void */ - private function getPubDirectory($dirType) + private function setPubDirectory($dirType) { - if (!$this->pubDirectory) { - $this->pubDirectory = ObjectManager::getInstance()->get(Filesystem::class)->getDirectoryRead($dirType); - } - return $this->pubDirectory; + $this->pubDirectoryRead = $this->pubDirectory->getDirectoryRead($dirType); } /** @@ -639,7 +639,10 @@ public function varDirective($construction) return $construction[0]; } - list($directive, $modifiers) = $this->explodeModifiers($construction[2], 'escape'); + list($directive, $modifiers) = $this->explodeModifiers( + $construction[2] . ($construction['filters'] ?? ''), + 'escape' + ); return $this->applyModifiers($this->getVariable($directive, ''), $modifiers); } @@ -656,6 +659,7 @@ public function varDirective($construction) * @param string $value * @param string $default assumed modifier if none present * @return array + * @deprecated Use the new FilterApplier or Directive Processor interfaces */ protected function explodeModifiers($value, $default = null) { @@ -674,6 +678,7 @@ protected function explodeModifiers($value, $default = null) * @param string $value * @param string $modifiers * @return string + * @deprecated Use the new FilterApplier or Directive Processor interfaces */ protected function applyModifiers($value, $modifiers) { @@ -701,6 +706,7 @@ protected function applyModifiers($value, $modifiers) * @param string $value * @param string $type * @return string + * @deprecated Use the new FilterApplier or Directive Processor interfaces */ public function modifierEscape($value, $type = 'html') { @@ -844,7 +850,7 @@ public function cssDirective($construction) return '/* ' . __('"file" parameter must be specified') . ' */'; } - $css = $this->getCssProcessor()->process( + $css = $this->cssProcessor->process( $this->getCssFilesContent([$params['file']]) ); @@ -947,9 +953,9 @@ public function getCssFilesContent(array $files) try { foreach ($files as $file) { $asset = $this->_assetRepo->createAsset($file, $designParams); - $pubDirectory = $this->getPubDirectory($asset->getContext()->getBaseDirType()); - if ($pubDirectory->isExist($asset->getPath())) { - $css .= $pubDirectory->readFile($asset->getPath()); + $this->setPubDirectory($asset->getContext()->getBaseDirType()); + if ($this->pubDirectoryRead->isExist($asset->getPath())) { + $css .= $this->pubDirectoryRead->readFile($asset->getPath()); } else { $css .= $asset->getContent(); } @@ -979,7 +985,7 @@ public function applyInlineCss($html) $cssToInline = $this->getCssFilesContent( $this->getInlineCssFiles() ); - $cssToInline = $this->getCssProcessor()->process($cssToInline); + $cssToInline = $this->cssProcessor->process($cssToInline); // Only run Emogrify if HTML and CSS contain content if ($html && $cssToInline) { diff --git a/app/code/Magento/Email/Model/Transport.php b/app/code/Magento/Email/Model/Transport.php index cbce1682cb5fa..79ceb56a8834d 100644 --- a/app/code/Magento/Email/Model/Transport.php +++ b/app/code/Magento/Email/Model/Transport.php @@ -9,7 +9,6 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\MailException; -use Magento\Framework\Mail\EmailMessageInterface; use Magento\Framework\Mail\MessageInterface; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Phrase; @@ -62,12 +61,12 @@ class Transport implements TransportInterface private $message; /** - * @param EmailMessageInterface $message Email message object + * @param MessageInterface $message Email message object * @param ScopeConfigInterface $scopeConfig Core store config * @param null|string|array|\Traversable $parameters Config options for sendmail parameters */ public function __construct( - EmailMessageInterface $message, + MessageInterface $message, ScopeConfigInterface $scopeConfig, $parameters = null ) { diff --git a/app/code/Magento/Email/Setup/Patch/Data/FlagLegacyTemplates.php b/app/code/Magento/Email/Setup/Patch/Data/FlagLegacyTemplates.php new file mode 100644 index 0000000000000..6a97f38239bd6 --- /dev/null +++ b/app/code/Magento/Email/Setup/Patch/Data/FlagLegacyTemplates.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Email\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Flag all existing email templates overrides as legacy + */ +class FlagLegacyTemplates implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup) + { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritDoc + */ + public function apply() + { + $this->moduleDataSetup + ->getConnection() + ->update($this->moduleDataSetup->getTable('email_template'), ['is_legacy' => '1']); + } + + /** + * @inheritDoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml index 1155930dd75ef..c859b956810c7 100644 --- a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml +++ b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml @@ -21,14 +21,14 @@ <amOnPage url="{{AdminEmailTemplateIndexPage.url}}" stepKey="navigateToEmailTemplatePage"/> <!--Click "Add New Template" button--> <click selector="{{AdminMainActionsSection.add}}" stepKey="clickAddNewTemplateButton"/> - <!--Select value for "Template" drop-down menu in "Load default template" tab--> + <!--Select value for "Template" drop-down menu in "Load Default Template" tab--> <selectOption selector="{{AdminEmailTemplateEditSection.templateDropDown}}" userInput="Registry Update" stepKey="selectValueFromTemplateDropDown"/> <!--Fill in required fields in "Template Information" tab and click "Save Template" button--> <click selector="{{AdminEmailTemplateEditSection.loadTemplateButton}}" stepKey="clickLoadTemplateButton"/> <fillField selector="{{AdminEmailTemplateEditSection.templateCode}}" userInput="{{EmailTemplate.templateName}}" stepKey="fillTemplateNameField"/> <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveTemplateButton"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the email template." stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the email template." stepKey="seeSuccessMessage"/> </actionGroup> <!--Create New Custom Template --> @@ -63,8 +63,8 @@ <click selector="{{AdminEmailTemplateEditSection.deleteTemplateButton}}" after="checkTemplateName" stepKey="deleteTemplate"/> <waitForElementVisible selector="{{AdminEmailTemplateEditSection.acceptPopupButton}}" after="deleteTemplate" stepKey="waitForConfirmButton"/> <click selector="{{AdminEmailTemplateEditSection.acceptPopupButton}}" after="waitForConfirmButton" stepKey="acceptPopup"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" after="acceptPopup" stepKey="waitForSuccessMessage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the email template." after="waitForSuccessMessage" stepKey="seeSuccessfulMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" after="acceptPopup" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the email template." after="waitForSuccessMessage" stepKey="seeSuccessfulMessage"/> </actionGroup> <actionGroup name="PreviewEmailTemplate" extends="FindAndOpenEmailTemplate"> diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php index 286e9a989d4d8..4d168ffbf2bdc 100644 --- a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php @@ -18,26 +18,42 @@ class PreviewTest extends \PHPUnit\Framework\TestCase const MALICIOUS_TEXT = 'test malicious'; + /** + * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + */ + protected $request; + + /** + * @var \Magento\Email\Block\Adminhtml\Template\Preview + */ + protected $preview; + + /** + * @var \Magento\Framework\Filter\Input\MaliciousCode|\PHPUnit_Framework_MockObject_MockObject + */ + protected $maliciousCode; + + /** + * @var \Magento\Email\Model\Template|\PHPUnit_Framework_MockObject_MockObject + */ + protected $template; + + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $storeManager; + /** * Init data */ protected function setUp() { $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - } - /** - * Check of processing email templates - * - * @param array $requestParamMap - * - * @dataProvider toHtmlDataProvider - * @param $requestParamMap - */ - public function testToHtml($requestParamMap) - { $storeId = 1; - $template = $this->getMockBuilder(\Magento\Email\Model\Template::class) + $designConfigData = []; + + $this->template = $this->getMockBuilder(\Magento\Email\Model\Template::class) ->setMethods( [ 'setDesignConfig', @@ -50,71 +66,106 @@ public function testToHtml($requestParamMap) ) ->disableOriginalConstructor() ->getMock(); - $template->expects($this->once()) + + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + + $this->maliciousCode = $this->createPartialMock( + \Magento\Framework\Filter\Input\MaliciousCode::class, + ['filter'] + ); + + $this->template->expects($this->once()) ->method('getProcessedTemplate') ->with($this->equalTo([])) ->willReturn(self::MALICIOUS_TEXT); - $designConfigData = []; - $template->expects($this->atLeastOnce()) - ->method('getDesignConfig') + + $this->template->method('getDesignConfig') ->willReturn(new \Magento\Framework\DataObject($designConfigData)); + $emailFactory = $this->createPartialMock(\Magento\Email\Model\TemplateFactory::class, ['create']); $emailFactory->expects($this->any()) ->method('create') - ->willReturn($template); + ->willReturn($this->template); - $request = $this->createMock(\Magento\Framework\App\RequestInterface::class); - $request->expects($this->any())->method('getParam')->willReturnMap($requestParamMap); $eventManage = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $design = $this->createMock(\Magento\Framework\View\DesignInterface::class); $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeup']); - $store->expects($this->any())->method('getId')->willReturn($storeId); - $storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $storeManager->expects($this->atLeastOnce()) - ->method('getDefaultStoreView') + + $store->expects($this->any()) + ->method('getId') + ->willReturn($storeId); + + $this->storeManager->method('getDefaultStoreView') ->willReturn($store); - $storeManager->expects($this->any())->method('getDefaultStoreView')->willReturn(null); - $storeManager->expects($this->any())->method('getStores')->willReturn([$store]); + + $this->storeManager->expects($this->any())->method('getDefaultStoreView')->willReturn(null); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]); $appState = $this->getMockBuilder(\Magento\Framework\App\State::class) - ->setConstructorArgs([$scopeConfig]) + ->setConstructorArgs( + [ + $scopeConfig + ] + ) ->setMethods(['emulateAreaCode']) ->disableOriginalConstructor() ->getMock(); $appState->expects($this->any()) ->method('emulateAreaCode') - ->with(\Magento\Email\Model\AbstractTemplate::DEFAULT_DESIGN_AREA, [$template, 'getProcessedTemplate']) - ->willReturn($template->getProcessedTemplate()); + ->with( + \Magento\Email\Model\AbstractTemplate::DEFAULT_DESIGN_AREA, + [$this->template, 'getProcessedTemplate'] + ) + ->willReturn($this->template->getProcessedTemplate()); $context = $this->createPartialMock( \Magento\Backend\Block\Template\Context::class, ['getRequest', 'getEventManager', 'getScopeConfig', 'getDesignPackage', 'getStoreManager', 'getAppState'] ); - $context->expects($this->any())->method('getRequest')->willReturn($request); + $context->expects($this->any())->method('getRequest')->willReturn($this->request); $context->expects($this->any())->method('getEventManager')->willReturn($eventManage); $context->expects($this->any())->method('getScopeConfig')->willReturn($scopeConfig); $context->expects($this->any())->method('getDesignPackage')->willReturn($design); - $context->expects($this->any())->method('getStoreManager')->willReturn($storeManager); + $context->expects($this->any())->method('getStoreManager')->willReturn($this->storeManager); $context->expects($this->once())->method('getAppState')->willReturn($appState); - $maliciousCode = $this->createPartialMock(\Magento\Framework\Filter\Input\MaliciousCode::class, ['filter']); - $maliciousCode->expects($this->once()) - ->method('filter') - ->with($this->equalTo($requestParamMap[1][2])) - ->willReturn(self::MALICIOUS_TEXT); - /** @var \Magento\Email\Block\Adminhtml\Template\Preview $preview */ - $preview = $this->objectManagerHelper->getObject( + $this->preview = $this->objectManagerHelper->getObject( \Magento\Email\Block\Adminhtml\Template\Preview::class, [ 'context' => $context, - 'maliciousCode' => $maliciousCode, + 'maliciousCode' => $this->maliciousCode, 'emailFactory' => $emailFactory ] ); - $this->assertEquals(self::MALICIOUS_TEXT, $preview->toHtml()); + } + + /** + * Check of processing email templates + * + * @param array $requestParamMap + * @dataProvider toHtmlDataProvider + */ + public function testToHtml($requestParamMap) + { + $this->request->expects($this->any()) + ->method('getParam') + ->willReturnMap($requestParamMap); + $this->template + ->expects($this->atLeastOnce()) + ->method('getDesignConfig'); + $this->storeManager->expects($this->atLeastOnce()) + ->method('getDefaultStoreView'); + $this->maliciousCode->expects($this->once()) + ->method('filter') + ->with($this->equalTo($requestParamMap[1][2])) + ->willReturn(self::MALICIOUS_TEXT); + + $this->assertEquals(self::MALICIOUS_TEXT, $this->preview->toHtml()); } /** diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Render/SenderTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Render/SenderTest.php new file mode 100644 index 0000000000000..4ca330f87a6ef --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Render/SenderTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Email\Test\Unit\Block\Adminhtml\Template\Render; + +use Magento\Email\Block\Adminhtml\Template\Grid\Renderer\Sender; +use Magento\Framework\DataObject; + +/** + * Class \Magento\Email\Test\Unit\Block\Adminhtml\Template\Render\SenderTest + */ +class SenderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Sender + */ + protected $block; + + /** + * Setup environment + */ + protected function setUp() + { + $this->block = $this->getMockBuilder(Sender::class) + ->disableOriginalConstructor() + ->setMethods(['escapeHtml']) + ->getMock(); + } + + /** + * Test render() with sender name and sender email are not empty + */ + public function testRenderWithSenderNameAndEmail() + { + $templateSenderEmail = 'test'; + $this->block->expects($this->any())->method('escapeHtml')->with($templateSenderEmail) + ->willReturn('test'); + $actualResult = $this->block->render( + new DataObject( + [ + 'template_sender_name' => 'test', + 'template_sender_email' => 'test@localhost.com' + ] + ) + ); + $this->assertEquals('test [test@localhost.com]', $actualResult); + } + + /** + * Test render() with sender name and sender email are empty + */ + public function testRenderWithNoSenderNameAndEmail() + { + $templateSenderEmail = ''; + $this->block->expects($this->any())->method('escapeHtml')->with($templateSenderEmail) + ->willReturn(''); + $actualResult = $this->block->render( + new DataObject( + [ + 'template_sender_name' => '', + 'template_sender_email' => '' + ] + ) + ); + $this->assertEquals('---', $actualResult); + } +} diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Render/TypeTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Render/TypeTest.php new file mode 100644 index 0000000000000..88eff38c81799 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/Render/TypeTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Email\Test\Unit\Block\Adminhtml\Template\Render; + +use Magento\Email\Block\Adminhtml\Template\Grid\Renderer\Type; +use Magento\Framework\DataObject; +use Magento\Framework\Phrase; + +/** + * Class \Magento\Email\Test\Unit\Block\Adminhtml\Template\Render\TypeTest + */ +class TypeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Type + */ + protected $block; + + /** + * Setup environment + */ + protected function setUp() + { + $this->block = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->setMethods(['__']) + ->getMock(); + } + + /** + * Test render() with supported template Text type + */ + public function testRenderWithSupportedTemplateTextType() + { + $testCase = [ + 'dataset' => [ + 'template_type' => '1' + ], + 'expectedResult' => 'Text' + ]; + $this->executeTestCase($testCase); + } + + /** + * Test render() with supported template HTML type + */ + public function testRenderWithSupportedTemplateHtmlType() + { + $testCase = [ + 'dataset' => [ + 'template_type' => '2' + ], + 'expectedResult' => 'HTML' + ]; + $this->executeTestCase($testCase); + } + + /** + * Test render() with unsupported template type + */ + public function testRenderWithUnsupportedTemplateType() + { + $testCase = [ + 'dataset' => [ + 'template_type' => '5' + ], + 'expectedResult' => 'Unknown' + ]; + $this->executeTestCase($testCase); + } + + /** + * Execute Test case + * + * @param array $testCase + */ + public function executeTestCase($testCase) + { + $actualResult = $this->block->render( + new DataObject( + [ + 'template_type' => $testCase['dataset']['template_type'], + ] + ) + ); + $this->assertEquals(new Phrase($testCase['expectedResult']), $actualResult); + } +} diff --git a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php index 9a67bf59dd4bf..d4584ce86dff2 100644 --- a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php +++ b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php @@ -54,11 +54,6 @@ class PreviewTest extends \PHPUnit\Framework\TestCase */ protected $pageTitleMock; - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - protected function setUp() { $objectManager = new ObjectManager($this); @@ -84,16 +79,11 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class) - ->setMethods(['setHeader']) - ->getMockForAbstractClass(); - $this->context = $objectManager->getObject( \Magento\Backend\App\Action\Context::class, [ 'request' => $this->requestMock, - 'view' => $this->viewMock, - 'response' => $this->responseMock + 'view' => $this->viewMock ] ); $this->object = $objectManager->getObject( @@ -118,9 +108,6 @@ public function testExecute() $this->pageTitleMock->expects($this->once()) ->method('prepend') ->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHeader') - ->with('Content-Security-Policy', "script-src 'self'"); $this->assertNull($this->object->execute()); } diff --git a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php index 0a0e9f780351d..036ab1b273fb0 100644 --- a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php @@ -117,8 +117,8 @@ protected function setUp() /** * Return the model under test with additional methods mocked. * - * @param array $mockedMethods - * @param array $data + * @param array $mockedMethods + * @param array $data * @return \Magento\Email\Model\Template|\PHPUnit_Framework_MockObject_MockObject */ protected function getModelMock(array $mockedMethods = [], array $data = []) @@ -150,17 +150,18 @@ protected function getModelMock(array $mockedMethods = [], array $data = []) } /** - * @param $variables array - * @param $templateType string - * @param $storeId int - * @param $expectedVariables array - * @param $expectedResult string + * @param $variables array + * @param $templateType string + * @param $storeId int + * @param $expectedVariables array + * @param $expectedResult string * @dataProvider getProcessedTemplateProvider */ public function testGetProcessedTemplate($variables, $templateType, $storeId, $expectedVariables, $expectedResult) { $filterTemplate = $this->getMockBuilder(\Magento\Email\Model\Template\Filter::class) - ->setMethods([ + ->setMethods( + [ 'setUseSessionInUrl', 'setPlainTemplateMode', 'setIsChildTemplate', @@ -170,7 +171,9 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e 'filter', 'getStoreId', 'getInlineCssFiles', - ]) + 'setStrictMode', + ] + ) ->disableOriginalConstructor() ->getMock(); @@ -194,20 +197,27 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e $filterTemplate->expects($this->any()) ->method('getStoreId') ->will($this->returnValue($storeId)); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(true)], [$this->equalTo(false)]) + ->willReturnOnConsecutiveCalls(false, true); $expectedVariables['store'] = $this->store; - $model = $this->getModelMock([ + $model = $this->getModelMock( + [ 'getDesignParams', 'applyDesignConfig', 'getTemplateText', 'isPlain', - ]); + ] + ); $filterTemplate->expects($this->any()) ->method('setVariables') ->with(array_merge(['this' => $model], $expectedVariables)); $model->setTemplateFilter($filterTemplate); $model->setTemplateType($templateType); + $model->setTemplateId('123'); $designParams = [ 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, @@ -241,7 +251,8 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e public function testGetProcessedTemplateException() { $filterTemplate = $this->getMockBuilder(\Magento\Email\Model\Template\Filter::class) - ->setMethods([ + ->setMethods( + [ 'setUseSessionInUrl', 'setPlainTemplateMode', 'setIsChildTemplate', @@ -251,7 +262,9 @@ public function testGetProcessedTemplateException() 'filter', 'getStoreId', 'getInlineCssFiles', - ]) + 'setStrictMode', + ] + ) ->disableOriginalConstructor() ->getMock(); $filterTemplate->expects($this->once()) @@ -272,13 +285,19 @@ public function testGetProcessedTemplateException() $filterTemplate->expects($this->any()) ->method('getStoreId') ->will($this->returnValue(1)); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(false)], [$this->equalTo(true)]) + ->willReturnOnConsecutiveCalls(true, false); - $model = $this->getModelMock([ + $model = $this->getModelMock( + [ 'getDesignParams', 'applyDesignConfig', 'getTemplateText', 'isPlain', - ]); + ] + ); $designParams = [ 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, @@ -290,6 +309,7 @@ public function testGetProcessedTemplateException() ->will($this->returnValue($designParams)); $model->setTemplateFilter($filterTemplate); $model->setTemplateType(\Magento\Framework\App\TemplateTypesInterface::TYPE_TEXT); + $model->setTemplateId('abc'); $filterTemplate->expects($this->once()) ->method('filter') @@ -361,9 +381,9 @@ public function testGetDefaultEmailLogo() } /** - * @param array $config + * @param array $config * @expectedException \Magento\Framework\Exception\LocalizedException - * @dataProvider invalidInputParametersDataProvider + * @dataProvider invalidInputParametersDataProvider */ public function testSetDesignConfigWithInvalidInputParametersThrowsException($config) { diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php index 2c9fdae111fd8..778573396b011 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Email\Test\Unit\Model\Template; use Magento\Email\Model\Template\Css\Processor; @@ -14,6 +17,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class FilterTest extends \PHPUnit\Framework\TestCase { @@ -92,6 +96,31 @@ class FilterTest extends \PHPUnit\Framework\TestCase */ private $cssInliner; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Email\Model\Template\Css\Processor + */ + private $cssProcessor; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Filesystem + */ + private $pubDirectory; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Filesystem\Directory\Read + */ + private $pubDirectoryRead; + + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\Filter\VariableResolver\StrategyResolver + */ + private $variableResolver; + + /** + * @var array + */ + private $directiveProcessors; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -147,6 +176,41 @@ protected function setUp() $this->cssInliner = $this->objectManager->getObject( \Magento\Framework\Css\PreProcessor\Adapter\CssInliner::class ); + + $this->cssProcessor = $this->getMockBuilder(\Magento\Email\Model\Template\Css\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->pubDirectory = $this->getMockBuilder(\Magento\Framework\Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->pubDirectoryRead = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Read::class) + ->disableOriginalConstructor() + ->getMock(); + $this->variableResolver = + $this->getMockBuilder(\Magento\Framework\Filter\VariableResolver\StrategyResolver::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->directiveProcessors = [ + 'depend' => + $this->getMockBuilder(\Magento\Framework\Filter\DirectiveProcessor\DependDirective::class) + ->disableOriginalConstructor() + ->getMock(), + 'if' => + $this->getMockBuilder(\Magento\Framework\Filter\DirectiveProcessor\IfDirective::class) + ->disableOriginalConstructor() + ->getMock(), + 'template' => + $this->getMockBuilder(\Magento\Framework\Filter\DirectiveProcessor\TemplateDirective::class) + ->disableOriginalConstructor() + ->getMock(), + 'legacy' => + $this->getMockBuilder(\Magento\Framework\Filter\DirectiveProcessor\LegacyDirective::class) + ->disableOriginalConstructor() + ->getMock(), + ]; } /** @@ -173,89 +237,16 @@ protected function getModel($mockedMethods = null) $this->configVariables, [], $this->cssInliner, + $this->directiveProcessors, + $this->variableResolver, + $this->cssProcessor, + $this->pubDirectory ] ) ->setMethods($mockedMethods) ->getMock(); } - /** - * Tests proper parsing of the {{trans ...}} directive used in email templates - * - * @dataProvider transDirectiveDataProvider - * @param $value - * @param $expected - * @param array $variables - */ - public function testTransDirective($value, $expected, array $variables = []) - { - $filter = $this->getModel()->setVariables($variables); - $this->assertEquals($expected, $filter->filter($value)); - } - - /** - * Data provider for various possible {{trans ...}} usages - * - * @return array - */ - public function transDirectiveDataProvider() - { - return [ - 'empty directive' => [ - '{{trans}}', - '', - ], - - 'empty string' => [ - '{{trans ""}}', - '', - ], - - 'no padding' => [ - '{{trans"Hello cruel coder..."}}', - 'Hello cruel coder...', - ], - - 'multi-line padding' => [ - "{{trans \t\n\r'Hello cruel coder...' \t\n\r}}", - 'Hello cruel coder...', - ], - - 'capture escaped double-quotes inside text' => [ - '{{trans "Hello \"tested\" world!"}}', - 'Hello "tested" world!', - ], - - 'capture escaped single-quotes inside text' => [ - "{{trans 'Hello \\'tested\\' world!'|escape}}", - "Hello 'tested' world!", - ], - - 'basic var' => [ - '{{trans "Hello %adjective world!" adjective="tested"}}', - 'Hello tested world!', - ], - - 'auto-escaped output' => [ - '{{trans "Hello %adjective <strong>world</strong>!" adjective="<em>bad</em>"}}', - 'Hello <em>bad</em> <strong>world</strong>!', - ], - - 'unescaped modifier' => [ - '{{trans "Hello %adjective <strong>world</strong>!" adjective="<em>bad</em>"|raw}}', - 'Hello <em>bad</em> <strong>world</strong>!', - ], - - 'variable replacement' => [ - '{{trans "Hello %adjective world!" adjective="$mood"}}', - 'Hello happy world!', - [ - 'mood' => 'happy' - ], - ], - ]; - } - /** * Test basic usages of applyInlineCss * @@ -329,17 +320,16 @@ public function testGetCssFilesContent() ->with($file, $designParams) ->willReturn($asset); - $pubDirectory = $this->getMockBuilder(ReadInterface::class) - ->getMockForAbstractClass(); - $reflectionClass = new \ReflectionClass(Filter::class); - $reflectionProperty = $reflectionClass->getProperty('pubDirectory'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($filter, $pubDirectory); - $pubDirectory->expects($this->once()) + $this->pubDirectory + ->expects($this->once()) + ->method('getDirectoryRead') + ->willReturn($this->pubDirectoryRead); + + $this->pubDirectoryRead->expects($this->once()) ->method('isExist') ->with($path . DIRECTORY_SEPARATOR . $file) ->willReturn(true); - $pubDirectory->expects($this->once()) + $this->pubDirectoryRead->expects($this->once()) ->method('readFile') ->with($path . DIRECTORY_SEPARATOR . $file) ->willReturn($css); @@ -396,43 +386,6 @@ public function testApplyInlineCssThrowsExceptionWhenDesignParamsNotSet() $filter->applyInlineCss('test'); } - /** - * Ensure that after filter callbacks are reset after exception is thrown during filtering - */ - public function testAfterFilterCallbackGetsResetWhenExceptionTriggered() - { - $value = '{{var random_var}}'; - $exception = new \Exception('Test exception'); - $exceptionResult = sprintf(__('Error filtering template: %s'), $exception->getMessage()); - - $this->appState->expects($this->once()) - ->method('getMode') - ->will($this->returnValue(\Magento\Framework\App\State::MODE_DEVELOPER)); - $this->logger->expects($this->once()) - ->method('critical') - ->with($exception); - - $filter = $this->getModel(['varDirective', 'resetAfterFilterCallbacks']); - $filter->expects($this->once()) - ->method('varDirective') - ->will($this->throwException($exception)); - - // Callbacks must be reset after exception is thrown - $filter->expects($this->once()) - ->method('resetAfterFilterCallbacks'); - - // Build arbitrary object to pass into the addAfterFilterCallback method - $callbackObject = $this->getMockBuilder('stdObject') - ->setMethods(['afterFilterCallbackMethod']) - ->getMock(); - // Callback should never run due to exception happening during filtering - $callbackObject->expects($this->never()) - ->method('afterFilterCallbackMethod'); - $filter->addAfterFilterCallback([$callbackObject, 'afterFilterCallbackMethod']); - - $this->assertEquals($exceptionResult, $filter->filter($value)); - } - public function testConfigDirectiveAvailable() { $path = "web/unsecure/base_url"; diff --git a/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php index 12b970f623e2d..cda3a4f4a1934 100644 --- a/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php @@ -514,14 +514,20 @@ public function testGetProcessedTemplateSubject() $templateSubject = 'templateSubject'; $model->setTemplateSubject($templateSubject); + $model->setTemplateId('123'); + class_exists(Template::class, true); $filterTemplate = $this->getMockBuilder(\Magento\Framework\Filter\Template::class) - ->setMethods(['setVariables', 'setStoreId', 'filter']) + ->setMethods(['setVariables', 'setStoreId', 'filter', 'setStrictMode']) ->disableOriginalConstructor() ->getMock(); $model->expects($this->once()) ->method('getTemplateFilter') ->willReturn($filterTemplate); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(true)], [$this->equalTo(false)]) + ->willReturnOnConsecutiveCalls(false, true); $model->expects($this->once()) ->method('applyDesignConfig'); diff --git a/app/code/Magento/Email/Test/Unit/ViewModel/Template/Preview/FormTest.php b/app/code/Magento/Email/Test/Unit/ViewModel/Template/Preview/FormTest.php new file mode 100644 index 0000000000000..88c323c923c45 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/ViewModel/Template/Preview/FormTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Email\Test\Unit\ViewModel\Template\Preview; + +use Magento\Email\ViewModel\Template\Preview\Form; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class FormTest + * + * @covers \Magento\Email\ViewModel\Template\Preview\Form + */ +class FormTest extends \PHPUnit\Framework\TestCase +{ + /** @var Form */ + protected $form; + + /** @var Http|\PHPUnit_Framework_MockObject_MockObject */ + protected $requestMock; + + protected function setUp() + { + $this->requestMock = $this->createPartialMock( + Http::class, + ['getParam', 'getMethod'] + ); + + $objectManagerHelper = new ObjectManager($this); + + $this->form = $objectManagerHelper->getObject( + Form::class, + ['request'=> $this->requestMock] + ); + } + + /** + * Tests that the form is created with the expected fields based on the request type. + * + * @dataProvider getFormFieldsDataProvider + * @param string $httpMethod + * @param array $httpParams + * @param array $expectedFields + * @throws LocalizedException + */ + public function testGetFormFields(string $httpMethod, array $httpParams, array $expectedFields) + { + $this->requestMock->expects($this->once()) + ->method('getMethod') + ->willReturn($httpMethod); + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap($httpParams); + + $actualFields = $this->form->getFormFields(); + + $this->assertEquals($expectedFields, $actualFields); + } + + /** + * Tests that an exception is thrown when a required parameter is missing for the request type. + * + * @dataProvider getFormFieldsInvalidDataProvider + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Missing expected parameter + * @param string $httpMethod + * @param array $httpParams + */ + public function testGetFormFieldsMissingParameter(string $httpMethod, array $httpParams) + { + $this->requestMock->expects($this->once()) + ->method('getMethod') + ->willReturn($httpMethod); + + $this->requestMock->expects($this->once()) + ->method('getParam') + ->willReturnMap($httpParams); + + $this->form->getFormFields(); + } + + /** + * @return array + */ + public function getFormFieldsDataProvider() + { + return [ + 'get_request_valid' => [ + 'httpMethod' => 'GET', + 'httpParams' => [ + ['id', null, 1] + ], + 'expectedFields' => [ + 'id' => 1 + ] + ], + 'get_request_valid_ignore_params' => [ + 'httpMethod' => 'GET', + 'httpParams' => [ + ['id', null, 1], + ['text', null, 'Hello World'], + ['type', null, 2], + ['styles', null, ''] + ], + 'expectedFields' => [ + 'id' => 1 + ] + ], + 'post_request_valid' => [ + 'httpMethod' => 'POST', + 'httpParams' => [ + ['text', null, 'Hello World'], + ['type', null, 2], + ['styles', null, ''] + ], + 'expectedFields' => [ + 'text' => 'Hello World', + 'type' => 2, + 'styles' => '' + ] + ] + ]; + } + + /** + * @return array + */ + public function getFormFieldsInvalidDataProvider() + { + return [ + 'get_request_missing_id' => [ + 'httpMethod' => 'GET', + 'httpParams' => [ + ['text', null, 'Hello World'], + ['type', null, 2], + ['styles', null, ''] + ] + ], + 'post_request_missing_text' => [ + 'httpMethod' => 'POST', + 'httpParams' => [ + ['type', null, 2], + ['styles', null, ''] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Email/ViewModel/Template/Preview/Form.php b/app/code/Magento/Email/ViewModel/Template/Preview/Form.php new file mode 100644 index 0000000000000..9db93cb94a299 --- /dev/null +++ b/app/code/Magento/Email/ViewModel/Template/Preview/Form.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\ViewModel\Template\Preview; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Class Form + */ +class Form implements ArgumentInterface +{ + private $expectedParamsGetRequest = [ + 'id' + ]; + + private $expectedParamsPostRequest = [ + 'text', + 'type', + 'styles' + ]; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + */ + public function __construct(RequestInterface $request) + { + $this->request = $request; + } + + /** + * Gets the fields to be included in the email preview form. + * + * @return array + * @throws LocalizedException + */ + public function getFormFields() + { + $params = $fields = []; + $method = $this->request->getMethod(); + + if ($method === 'GET') { + $params = $this->expectedParamsGetRequest; + } elseif ($method === 'POST') { + $params = $this->expectedParamsPostRequest; + } + + foreach ($params as $paramName) { + $fieldValue = $this->request->getParam($paramName); + if ($fieldValue === null) { + throw new LocalizedException( + __("Missing expected parameter \"$paramName\" while attempting to generate template preview.") + ); + } + $fields[$paramName] = $fieldValue; + } + + return $fields; + } +} diff --git a/app/code/Magento/Email/etc/db_schema.xml b/app/code/Magento/Email/etc/db_schema.xml index dbb8855e9e57e..82c5f18ddb9e9 100644 --- a/app/code/Magento/Email/etc/db_schema.xml +++ b/app/code/Magento/Email/etc/db_schema.xml @@ -27,6 +27,8 @@ <column xsi:type="varchar" name="orig_template_code" nullable="true" length="200" comment="Original Template Code"/> <column xsi:type="text" name="orig_template_variables" nullable="true" comment="Original Template Variables"/> + <column xsi:type="boolean" name="is_legacy" nullable="false" + default="false" comment="Should the template render in legacy mode"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="template_id"/> </constraint> diff --git a/app/code/Magento/Email/etc/di.xml b/app/code/Magento/Email/etc/di.xml index 9ec3e04728a2c..73ba21ed7537b 100644 --- a/app/code/Magento/Email/etc/di.xml +++ b/app/code/Magento/Email/etc/di.xml @@ -72,4 +72,22 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Filter\Template"> + <arguments> + <argument name="directiveProcessors" xsi:type="array"> + <item name="depend" sortOrder="100" xsi:type="object">Magento\Framework\Filter\DirectiveProcessor\DependDirective</item> + <item name="if" sortOrder="200" xsi:type="object">Magento\Framework\Filter\DirectiveProcessor\IfDirective</item> + <item name="template" sortOrder="300" xsi:type="object">Magento\Framework\Filter\DirectiveProcessor\TemplateDirective</item> + <item name="legacy" sortOrder="400" xsi:type="object">Magento\Framework\Filter\DirectiveProcessor\LegacyDirective</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\Filter\DirectiveProcessor\Filter\FilterPool"> + <arguments> + <argument name="filters" xsi:type="array"> + <item name="nl2br" xsi:type="object">Magento\Framework\Filter\DirectiveProcessor\Filter\NewlineToBreakFilter</item> + <item name="escape" xsi:type="object">Magento\Framework\Filter\DirectiveProcessor\Filter\EscapeFilter</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Email/i18n/en_US.csv b/app/code/Magento/Email/i18n/en_US.csv index 8eed4b5c662b5..412660d90d469 100644 --- a/app/code/Magento/Email/i18n/en_US.csv +++ b/app/code/Magento/Email/i18n/en_US.csv @@ -72,7 +72,7 @@ City,City "We're sorry, an error has occurred while generating this content.","We're sorry, an error has occurred while generating this content." "Invalid sender data","Invalid sender data" Title,Title -"Load default template","Load default template" +"Load Default Template","Load Default Template" Template,Template "Are you sure you want to strip tags?","Are you sure you want to strip tags?" "Are you sure you want to delete this template?","Are you sure you want to delete this template?" diff --git a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml index 97f31c618f9b7..e7cbc675ce386 100644 --- a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml +++ b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml @@ -12,7 +12,11 @@ <referenceContainer name="backend.page" remove="true"/> <referenceContainer name="menu.wrapper" remove="true"/> <referenceContainer name="root"> - <block name="preview.page.content" class="Magento\Backend\Block\Page" template="Magento_Email::preview/iframeswitcher.phtml"/> + <block name="preview.page.content" class="Magento\Backend\Block\Page" template="Magento_Email::preview/iframeswitcher.phtml"> + <arguments> + <argument name="preview_form_view_model" xsi:type="object">Magento\Email\ViewModel\Template\Preview\Form</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml index 4d26b59b093e2..29ceb71a138e4 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -7,13 +7,34 @@ /** @var \Magento\Backend\Block\Page $block */ ?> <div id="preview" class="cms-revision-preview"> - <iframe - name="preview_iframe" + <iframe name="preview_iframe" id="preview_iframe" frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%" - sandbox="allow-forms allow-pointer-lock" - src="<?= $block->escapeUrl($block->getUrl('*/*/popup', ['_current' => true])) ?>" - /> + sandbox="allow-same-origin allow-pointer-lock" + ></iframe> + <form id="preview_form" + action="<?= $block->escapeUrl($block->getUrl('*/*/popup')) ?>" + method="post" + target="preview_iframe" + > + <input type="hidden" name="form_key" value="<?= /* @noEscape */ $block->getFormKey() ?>" /> + <?php foreach ($block->getPreviewFormViewModel()->getFormFields() as $name => $value) : ?> + <input type="hidden" name="<?= $block->escapeHtmlAttr($name) ?>" value="<?= $block->escapeHtmlAttr($value) ?>"/> + <?php endforeach; ?> + </form> </div> +<script> +require([ + 'jquery' +], function($) { + $(document).ready(function() { + $('#preview_form').submit(); + }); + + $('#preview_iframe').load(function() { + $(this).height($(this).contents().height()); + }); +}); +</script> diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index 1f236a21a7306..7378fa4b2e47f 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -12,9 +12,9 @@ use Magento\Framework\App\TemplateTypesInterface; <form action="<?= $block->escapeUrl($block->getLoadUrl()) ?>" method="post" id="email_template_load_form"> <?= $block->getBlockHtml('formkey') ?> <fieldset class="admin__fieldset form-inline"> - <legend class="admin__legend"><span><?= $block->escapeHtml(__('Load default template')) ?></span></legend><br> - <div class="admin__field"> - <label class="admin__field-label" for="template_select"><?= $block->escapeHtml(__('Template')) ?></label> + <legend class="admin__legend"><span><?= $block->escapeHtml(__('Load Default Template')) ?></span></legend><br> + <div class="admin__field required"> + <label class="admin__field-label" for="template_select"><span><?= $block->escapeHtml(__('Template')) ?></span></label> <div class="admin__field-control"> <select id="template_select" name="code" class="admin__control-select required-entry"> <?php foreach ($block->getTemplateOptions() as $group => $options) : ?> @@ -48,7 +48,7 @@ use Magento\Framework\App\TemplateTypesInterface; <?= /* @noEscape */ $block->getFormHtml() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="get" id="email_template_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="email_template_preview_form" target="_blank"> <?= /* @noEscape */ $block->getBlockHtml('formkey') ?> <div class="no-display"> <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> @@ -153,6 +153,7 @@ require([ } else { $('preview_type').value = <?= (int) $block->getTemplateType() ?>; } + if (typeof tinyMCE == 'undefined' || !tinyMCE.get('template_text')) { $('preview_text').value = $('template_text').value; } else { diff --git a/app/code/Magento/Email/view/frontend/email/footer.html b/app/code/Magento/Email/view/frontend/email/footer.html index 40e18372252bc..b9db3b8597cad 100644 --- a/app/code/Magento/Email/view/frontend/email/footer.html +++ b/app/code/Magento/Email/view/frontend/email/footer.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Footer"}} @--> <!--@vars { -"var store.getFrontendName()":"Store Name" +"var store.frontend_name":"Store Name" } @--> <!-- End Content --> @@ -14,7 +14,7 @@ </tr> <tr> <td class="footer"> - <p class="closing">{{trans "Thank you, %store_name" store_name=$store.getFrontendName()}}!</p> + <p class="closing">{{trans "Thank you, %store_name" store_name=$store.frontend_name}}!</p> </td> </tr> </table> diff --git a/app/code/Magento/Email/view/frontend/email/header.html b/app/code/Magento/Email/view/frontend/email/header.html index c4f49698dc69b..45e299cb51d17 100644 --- a/app/code/Magento/Email/view/frontend/email/header.html +++ b/app/code/Magento/Email/view/frontend/email/header.html @@ -6,6 +6,8 @@ --> <!--@subject {{trans "Header"}} @--> <!--@vars { +"var logo_url":"Email Logo Image URL", +"var logo_alt":"Email Logo Alt Text", "var logo_height":"Email Logo Image Height", "var logo_width":"Email Logo Image Width", "var template_styles|raw":"Template CSS" diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyChangeKeyAutoActionGroup.xml b/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyChangeKeyAutoActionGroup.xml new file mode 100644 index 0000000000000..62568ebb551e1 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyChangeKeyAutoActionGroup.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="AdminEncryptionKeyChangeKeyAutoActionGroup"> + <annotations> + <description>Change Encryption Key Auto Generate Action Group.</description> + </annotations> + <arguments> + <argument name="encryptionKeyData" defaultValue="AdminEncryptionKeyAutoGenerate"/> + </arguments> + + <selectOption selector="{{AdminEncryptionKeyChangeFormSection.autoGenerate}}" userInput="{{encryptionKeyData.autoGenerate}}" stepKey="selectGenerateMode"/> + <click selector="{{AdminEncryptionKeyChangeFormSection.changeEncryptionKey}}" stepKey="clickChangeButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The encryption key has been changed." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyChangeKeyManualActionGroup.xml b/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyChangeKeyManualActionGroup.xml new file mode 100644 index 0000000000000..0880bd07a0739 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyChangeKeyManualActionGroup.xml @@ -0,0 +1,26 @@ +<?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="AdminEncryptionKeyChangeKeyManualActionGroup"> + <annotations> + <description>Change Encryption Key - No-Auto Generate Action Group.</description> + </annotations> + <arguments> + <argument name="encryptionKeyData" defaultValue="AdminEncryptionKeyManualGenerate"/> + </arguments> + + <selectOption selector="{{AdminEncryptionKeyChangeFormSection.autoGenerate}}" userInput="{{encryptionKeyData.autoGenerate}}" stepKey="selectGenerateMode"/> + <fillField selector="{{AdminEncryptionKeyChangeFormSection.cryptKey}}" userInput="{{encryptionKeyData.cryptKey}}" stepKey="fillCryptKey"/> + <click selector="{{AdminEncryptionKeyChangeFormSection.changeEncryptionKey}}" stepKey="clickChangeButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The encryption key has been changed." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyNavigateToChangePageActionGroup.xml b/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyNavigateToChangePageActionGroup.xml new file mode 100644 index 0000000000000..69847526a15a0 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/ActionGroup/AdminEncryptionKeyNavigateToChangePageActionGroup.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="AdminEncryptionKeyNavigateToChangePageActionGroup"> + <annotations> + <description>Navigate to change encryption key page.</description> + </annotations> + <amOnPage url="{{AdminEncryptionKeyChangeFormPage.url}}" stepKey="navigateToChangeEncryptionPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Data/AdminEncryptionKeyData.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Data/AdminEncryptionKeyData.xml new file mode 100644 index 0000000000000..ff1fe3fd2e10c --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Data/AdminEncryptionKeyData.xml @@ -0,0 +1,18 @@ +<?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="AdminEncryptionKeyAutoGenerate"> + <data key="autoGenerate">Yes</data> + </entity> + <entity name="AdminEncryptionKeyManualGenerate"> + <data key="autoGenerate">No</data> + <data key="cryptKey">9d469ae32ec27dfec0206cb5d63f135d</data> + </entity> +</entities> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Page/AdminEncryptionKeyChangeFormPage.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Page/AdminEncryptionKeyChangeFormPage.xml new file mode 100644 index 0000000000000..b3b8b33fc0364 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Page/AdminEncryptionKeyChangeFormPage.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="AdminEncryptionKeyChangeFormPage" url="admin/crypt_key/index" area="admin" module="Magento_EncryptionKey"> + <section name="AdminCustomerConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml new file mode 100644 index 0000000000000..7ce37af60fd7f --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.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="AdminEncryptionKeyChangeFormSection"> + <element name="autoGenerate" type="select" selector="#generate_random"/> + <element name="cryptKey" type="input" selector="#crypt_key"/> + <element name="changeEncryptionKey" type="button" selector=".page-actions-buttons #save" timeout="10"/> + </section> +</sections> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml new file mode 100644 index 0000000000000..ded57f4aad019 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.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="AdminEncryptionKeyAutoGenerateKeyTest"> + <annotations> + <features value="Encryption Key"/> + <stories value="Change Encryption Key"/> + <title value="Change Encryption Key by Auto Generate Key"/> + <description value="Change Encryption Key by Auto Generate Key"/> + <severity value="CRITICAL"/> + <group value="encryption_key"/> + </annotations> + + <before> + <!--Login to Admin Area--> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + + <after> + <!--Logout from Admin Area--> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="AdminEncryptionKeyNavigateToChangePageActionGroup" stepKey="navigateToPage"/> + <actionGroup ref="AdminEncryptionKeyChangeKeyAutoActionGroup" stepKey="changeKeyAutoGenerate"/> + </test> +</tests> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml new file mode 100644 index 0000000000000..f3a9849969263 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.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="AdminEncryptionKeyManualGenerateKeyTest"> + <annotations> + <features value="Encryption Key"/> + <stories value="Change Encryption Key"/> + <title value="Change Encryption Key by Manual Key"/> + <description value="Change Encryption Key by Manual Key"/> + <severity value="CRITICAL"/> + <group value="encryption_key"/> + </annotations> + + <before> + <!--Login to Admin Area--> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + + <after> + <!--Logout from Admin Area--> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="AdminEncryptionKeyNavigateToChangePageActionGroup" stepKey="navigateToPage"/> + <actionGroup ref="AdminEncryptionKeyChangeKeyManualActionGroup" stepKey="changeKeyManualGenerate"/> + </test> +</tests> diff --git a/app/code/Magento/Fedex/Test/Mftf/Data/FedExConfigData.xml b/app/code/Magento/Fedex/Test/Mftf/Data/FedExConfigData.xml new file mode 100644 index 0000000000000..d03403970ae55 --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Data/FedExConfigData.xml @@ -0,0 +1,49 @@ +<?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="AdminFedexEnableForCheckoutConfigData" type="fedex_config"> + <data key="path">carriers/fedex/active</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexEnableSandboxModeConfigData" type="fedex_config"> + <data key="path">carriers/fedex/sandbox_mode</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexEnableDebugConfigData" type="fedex_config"> + <data key="path">carriers/fedex/debug</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexEnableShowMethodConfigData" type="fedex_config"> + <data key="path">carriers/fedex/showmethod</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexDisableShowMethodConfigData" type="fedex_config"> + <data key="path">carriers/fedex/showmethod</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> + <entity name="AdminFedexDisableDebugConfigData" type="fedex_config"> + <data key="path">carriers/fedex/debug</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> + <entity name="AdminFedexDisableSandboxModeConfigData" type="fedex_config"> + <data key="path">carriers/fedex/sandbox_mode</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> + <entity name="AdminFedexDisableForCheckoutConfigData" type="fedex_config"> + <data key="path">carriers/fedex/active</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> +</entities> diff --git a/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml b/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml new file mode 100644 index 0000000000000..0f75d475d6b1b --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml @@ -0,0 +1,32 @@ +<?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="AdminShippingMethodFedExSection"> + <element name="carriersFedExTab" type="button" selector="#carriers_fedex-head"/> + <element name="carriersFedExActive" type="input" selector="#carriers_fedex_active_inherit"/> + <element name="carriersFedExTitle" type="input" selector="#carriers_fedex_title_inherit"/> + <element name="carriersFedExAccountId" type="input" selector="#carriers_fedex_account"/> + <element name="carriersFedExMeterNumber" type="input" selector="#carriers_fedex_meter_number"/> + <element name="carriersFedExKey" type="input" selector="#carriers_fedex_key"/> + <element name="carriersFedExPassword" type="input" selector="#carriers_fedex_password"/> + <element name="carriersFedExSandboxMode" type="input" selector="#carriers_fedex_sandbox_mode_inherit"/> + <element name="carriersFedExShipmentRequestType" type="input" selector="#carriers_fedex_shipment_requesttype_inherit"/> + <element name="carriersFedExPackaging" type="input" selector="#carriers_fedex_packaging_inherit"/> + <element name="carriersFedExDropoff" type="input" selector="#carriers_fedex_dropoff_inherit"/> + <element name="carriersFedExUnitOfMeasure" type="input" selector="#carriers_fedex_unit_of_measure_inherit"/> + <element name="carriersFedExMaxPackageWeight" type="input" selector="#carriers_fedex_max_package_weight_inherit"/> + <element name="carriersFedExHandlingType" type="input" selector="#carriers_fedex_handling_type_inherit"/> + <element name="carriersFedExHandlingAction" type="select" selector="#carriers_fedex_handling_action_inherit"/> + <element name="carriersFedExFreeMethod" type="input" selector="#carriers_fedex_free_method_inherit"/> + <element name="carriersFedExSpecificErrMsg" type="input" selector="#carriers_fedex_specificerrmsg_inherit"/> + <element name="carriersFedExAllowSpecific" type="input" selector="#carriers_fedex_sallowspecific_inherit"/> + <element name="carriersFedExSpecificCountry" type="input" selector="#carriers_fedex_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..f599d7ca223ae --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,52 @@ +<?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="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in FedEx section--> + <comment userInput="Assert configuration are disabled in FedEx section" stepKey="commentSeeDisabledFedExConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodFedExSection.carriersFedExTab}}" dependentSelector="{{AdminShippingMethodFedExSection.carriersFedExActive}}" visible="false" stepKey="expandFedExTab"/> + <waitForElementVisible selector="{{AdminShippingMethodFedExSection.carriersFedExActive}}" stepKey="waitFedExTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExActive}}" userInput="disabled" stepKey="grabFedExActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExActiveDisabled" stepKey="assertFedExActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExTitle}}" userInput="disabled" stepKey="grabFedExTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExTitleDisabled" stepKey="assertFedExTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExAccountId}}" userInput="disabled" stepKey="grabFedExAccountIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExAccountIdDisabled" stepKey="assertFedExAccountIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExMeterNumber}}" userInput="disabled" stepKey="grabFedExMeterNumberDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExMeterNumberDisabled" stepKey="assertFedExMeterNumberDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExKey}}" userInput="disabled" stepKey="grabFedExKeyDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExKeyDisabled" stepKey="assertFedExKeyDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExPassword}}" userInput="disabled" stepKey="grabFedExPasswordDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExPasswordDisabled" stepKey="assertFedExPasswordDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSandboxMode}}" userInput="disabled" stepKey="grabFedExSandboxDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExSandboxDisabled" stepKey="assertFedExSandboxDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExShipmentRequestType}}" userInput="disabled" stepKey="grabFedExShipmentRequestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExShipmentRequestTypeDisabled" stepKey="assertFedExShipmentRequestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExPackaging}}" userInput="disabled" stepKey="grabFedExPackagingDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExPackagingDisabled" stepKey="assertFedExPackagingDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExDropoff}}" userInput="disabled" stepKey="grabFedExDropoffDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExDropoffDisabled" stepKey="assertFedExDropoffDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExUnitOfMeasure}}" userInput="disabled" stepKey="grabFedExUnitOfMeasureDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExUnitOfMeasureDisabled" stepKey="assertFedExUnitOfMeasureDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExMaxPackageWeight}}" userInput="disabled" stepKey="grabFedExMaxPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExMaxPackageWeightDisabled" stepKey="assertFedExMaxPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExHandlingType}}" userInput="disabled" stepKey="grabFedExHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExHandlingTypeDisabled" stepKey="assertFedExHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExHandlingAction}}" userInput="disabled" stepKey="grabFedExHandlingActionDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExHandlingActionDisabled" stepKey="assertFedExHandlingActionDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSpecificErrMsg}}" userInput="disabled" stepKey="grabFedExSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExSpecificErrMsgDisabled" stepKey="assertFedExSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExAllowSpecific}}" userInput="disabled" stepKey="grabFedExAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExAllowSpecificDisabled" stepKey="assertFedExAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSpecificCountry}}" userInput="disabled" stepKey="grabFedExSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExSpecificCountryDisabled" stepKey="assertFedExSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml new file mode 100644 index 0000000000000..91a76383babd4 --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml @@ -0,0 +1,130 @@ +<?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="AdminCreatingShippingLabelTest"> + <annotations> + <features value="Fedex"/> + <stories value="Shipping label"/> + <title value="Creating shipping label"/> + <description value="Creating shipping label"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20287"/> + <useCaseId value="MC-18215"/> + <group value="shipping"/> + <skip> + <issueId value="MQE-1578"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create product --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Set Fedex configs data--> + <magentoCLI command="config:set {{AdminFedexEnableForCheckoutConfigData.path}} {{AdminFedexEnableForCheckoutConfigData.value}}" stepKey="enableCheckout"/> + <magentoCLI command="config:set {{AdminFedexEnableSandboxModeConfigData.path}} {{AdminFedexEnableSandboxModeConfigData.value}}" stepKey="enableSandbox"/> + <magentoCLI command="config:set {{AdminFedexEnableDebugConfigData.path}} {{AdminFedexEnableDebugConfigData.value}}" stepKey="enableDebug"/> + <magentoCLI command="config:set {{AdminFedexEnableShowMethodConfigData.path}} {{AdminFedexEnableShowMethodConfigData.value}}" stepKey="enableShowMethod"/> + <!--TODO: add fedex credentials--> + <!--Set StoreInformation configs data--> + <magentoCLI command="config:set {{AdminGeneralSetStoreNameConfigData.path}} '{{AdminGeneralSetStoreNameConfigData.value}}'" stepKey="setStoreInformationName"/> + <magentoCLI command="config:set {{AdminGeneralSetStorePhoneConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.telephone}}" stepKey="setStoreInformationPhone"/> + <magentoCLI command="config:set {{AdminGeneralSetCountryConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="setStoreInformationCountry"/> + <magentoCLI command="config:set {{AdminGeneralSetCityConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="setStoreInformationCity"/> + <magentoCLI command="config:set {{AdminGeneralSetPostcodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setStoreInformationPostcode"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setStoreInformationStreetAddress"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setStoreInformationStreetAddress2"/> + <magentoCLI command="config:set {{AdminGeneralSetVatNumberConfigData.path}} {{AdminGeneralSetVatNumberConfigData.value}}" stepKey="setStoreInformationVatNumber"/> + <!--Set Shipping settings origin data--> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCountryConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="setOriginCountry"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCityConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="setOriginCity"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setOriginZipCode"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setOriginStreetAddress"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setOriginStreetAddress2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!--Reset configs--> + <magentoCLI command="config:set {{AdminFedexDisableForCheckoutConfigData.path}} {{AdminFedexDisableForCheckoutConfigData.value}}" stepKey="disableCheckout"/> + <magentoCLI command="config:set {{AdminFedexDisableSandboxModeConfigData.path}} {{AdminFedexDisableSandboxModeConfigData.value}}" stepKey="disableSandbox"/> + <magentoCLI command="config:set {{AdminFedexDisableDebugConfigData.path}} {{AdminFedexDisableDebugConfigData.value}}" stepKey="disableDebug"/> + <magentoCLI command="config:set {{AdminFedexDisableShowMethodConfigData.path}} {{AdminFedexDisableShowMethodConfigData.value}}" stepKey="disableShowMethod"/> + <magentoCLI command="config:set {{AdminGeneralSetStoreNameConfigData.path}} ''" stepKey="setStoreInformationName"/> + <magentoCLI command="config:set {{AdminGeneralSetStorePhoneConfigData.path}} ''" stepKey="setStoreInformationPhone"/> + <magentoCLI command="config:set {{AdminGeneralSetCityConfigData.path}} ''" stepKey="setStoreInformationCity"/> + <magentoCLI command="config:set {{AdminGeneralSetPostcodeConfigData.path}} ''" stepKey="setStoreInformationPostcode"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddressConfigData.path}} ''" stepKey="setStoreInformationStreetAddress"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddress2ConfigData.path}} ''" stepKey="setStoreInformationStreetAddress2"/> + <magentoCLI command="config:set {{AdminGeneralSetVatNumberConfigData.path}} ''" stepKey="setStoreInformationVatNumber"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCityConfigData.path}} ''" stepKey="setOriginCity"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} ''" stepKey="setOriginZipCode"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} ''" stepKey="setOriginStreetAddress"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} ''" stepKey="setOriginStreetAddress2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Add country of manufacture to product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="amOnEditPage"/> + <waitForPageLoad stepKey="waitForEditPage"/> + <actionGroup ref="AdminFillProductCountryOfManufactureActionGroup" stepKey="fillCountryOfManufacture"> + <argument name="countryId" value="DE"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <!--Place for order using FedEx shipping method--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnStorefrontProductPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="addAddress"> + <argument name="customerVar" value="Simple_US_Utah_Customer"/> + <argument name="customerAddressVar" value="US_Address_California"/> + <argument name="shippingMethod" value="Federal Express"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!--Open created order in admin--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchOrder"> + <argument name="keyword" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <!--Create Invoice--> + <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createInvoice"/> + <!--Create shipping label--> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipmentIntoOrder"/> + <checkOption selector="{{AdminShipmentTotalSection.createShippingLabel}}" stepKey="checkCreateShippingLabel"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <actionGroup ref="AdminShipmentCreateShippingLabelActionGroup" stepKey="createPackage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AdminGoToShipmentTabActionGroup" stepKey="goToShipmentTab"/> + <click selector="{{AdminOrderShipmentsTabSection.viewGridRow('1')}}" stepKey="clickRowToViewShipment"/> + <waitForPageLoad stepKey="waitForShipmentItemsSection"/> + <seeElement selector="{{AdminShipmentTrackingInformationShippingSection.shippingInfoTable}}" stepKey="seeInformationTable"/> + <seeElement selector="{{AdminShipmentTrackingInformationShippingSection.shippingNumber}}" stepKey="seeShippingNumberElement"/> + <grabTextFrom selector="{{AdminShipmentTrackingInformationShippingSection.shippingMethod}}" stepKey="grabShippingMethod"/> + <grabTextFrom selector="{{AdminShipmentTrackingInformationShippingSection.shippingMethodTitle}}" stepKey="grabShippingMethodTitle"/> + <assertEquals actual="$grabShippingMethod" expectedType="string" expected="Federal Express" stepKey="assertShippingMethodIsFedEx"/> + <assertEquals actual="$grabShippingMethodTitle" expectedType="string" expected="Federal Express" stepKey="assertShippingMethodTitleIsFedEx"/> + </test> +</tests> diff --git a/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php b/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php index e02401c1a865d..59ad900c491b6 100644 --- a/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php +++ b/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php @@ -43,6 +43,15 @@ public function __construct(MessageHelper $helper) */ public function afterGetItemsBoxTextAfter(ShippingBlock $subject, $itemsBoxText, DataObject $addressEntity) { + if ($addressEntity->getGiftMessageId() === null) { + $addressEntity->setGiftMessageId($addressEntity->getQuote()->getGiftMessageId()); + } + foreach ($addressEntity->getAllItems() as $item) { + if ($item->getGiftMessageId() === null) { + $item->setGiftMessageId($item->getQuoteItem()->getGiftMessageId()); + } + } + return $itemsBoxText . $this->helper->getInline('multishipping_address', $addressEntity); } } diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/AdminOrderGiftSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/AdminOrderGiftSection.xml new file mode 100644 index 0000000000000..dc6d0b79a8367 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/AdminOrderGiftSection.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="AdminOrderGiftSection"> + <element name="orderItemGiftOptionsLink" type="text" selector="//table[contains(@class, 'edit-order-table')]//tbody[contains(.,'{{productName}}')]//a[contains(@class, 'action-link')]" parameterized="true"/> + <element name="orderItemGiftMessage" type="textarea" selector="#current_item_giftmessage_message" /> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftMessageSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftMessageSection.xml new file mode 100644 index 0000000000000..b1f6f35ba5d9c --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftMessageSection.xml @@ -0,0 +1,12 @@ +<?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="StorefrontCheckoutCartGiftMessageSection"> + <element name="giftItemMessage" type="textarea" selector="tbody.cart:nth-of-type({{blockNumber}}) #gift-message-whole-message" parameterized="true"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftSection.xml new file mode 100644 index 0000000000000..e39279de228f9 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftSection.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="StorefrontCheckoutCartGiftSection"> + <element name="cartItemGiftMessage" type="text" selector="//tbody[contains(.,'{{productName}}')]//div[@class='gift-message']//textarea" parameterized="true"/> + <element name="orderNumber" type="text" selector="(//div[contains(@class, 'orders-succeed')]//a)[{{blockNumber}}]" parameterized="true"/> + <element name="viewOrder" type="text" selector="//table[@id='my-orders-table']//tr[contains(.,'{{orderNumber}}')]//a[contains(@class, 'action view')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontOrderGiftSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontOrderGiftSection.xml new file mode 100644 index 0000000000000..45e7531f0b4a8 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontOrderGiftSection.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="StorefrontOrderGiftSection"> + <element name="giftMessageLink" type="button" selector=".table-wrapper.order-items .options .action.show"/> + <element name="giftMessage" type="text" selector=".order-gift-message .item-message" /> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventOrderToQuoteObserverTest.php b/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventOrderToQuoteObserverTest.php new file mode 100644 index 0000000000000..85c062d9cec11 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventOrderToQuoteObserverTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GiftMessage\Test\Unit\Observer; + +use Magento\Framework\Event; +use Magento\Framework\Message\MessageInterface; +use Magento\GiftMessage\Helper\Message; +use Magento\GiftMessage\Model\Message as MessageModel; +use Magento\GiftMessage\Model\MessageFactory; +use Magento\GiftMessage\Observer\SalesEventOrderToQuoteObserver; +use Magento\Framework\Event\Observer; +use Magento\Quote\Model\Quote; +use Magento\Sales\Model\Order; +use Magento\Store\Model\Store; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * SalesEventOrderToQuoteObserverTest + */ +class SalesEventOrderToQuoteObserverTest extends TestCase +{ + /** + * @var SalesEventOrderToQuoteObserver + */ + private $observer; + + /** + * @var MessageFactory|MockObject + */ + private $messageFactoryMock; + + /** + * @var Message|MockObject + */ + private $giftMessageMock; + + /** + * @var Observer|MockObject + */ + private $observerMock; + + /** + * @var Event|MockObject + */ + private $eventMock; + + /** + * @var Order|MockObject + */ + private $orderMock; + + /** + * @var Store|MockObject + */ + private $storeMock; + + /** + * @var MessageInterface|MockObject + */ + private $messageMock; + + /** + * @var Quote|MockObject + */ + private $quoteMock; + + /** + * @return void + */ + public function setUp(): void + { + $this->messageFactoryMock = $this->createMock(MessageFactory::class); + $this->giftMessageMock = $this->createMock(Message::class); + $this->observerMock = $this->createMock(Observer::class); + $this->eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getOrder', 'getQuote']) + ->getMock(); + $this->orderMock = $this->getMockBuilder(Order::class) + ->setMethods(['getReordered', 'getStore', 'getGiftMessageId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->setMethods(['setGiftMessageId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->storeMock = $this->createMock(Store::class); + $this->messageMock = $this->createMock(MessageModel::class); + + $this->observer = new SalesEventOrderToQuoteObserver( + $this->messageFactoryMock, + $this->giftMessageMock + ); + } + + /** + * Tests duplicating gift message from order to quote + * + * @dataProvider giftMessageDataProvider + * + * @param bool $orderIsReordered + * @param bool $isMessagesAllowed + */ + public function testExecute(bool $orderIsReordered, bool $isMessagesAllowed): void + { + $giftMessageId = 1; + $newGiftMessageId = 2; + + $this->eventMock + ->expects($this->atLeastOnce()) + ->method('getOrder') + ->willReturn($this->orderMock); + $this->observerMock + ->expects($this->atLeastOnce()) + ->method('getEvent') + ->willReturn($this->eventMock); + + if (!$orderIsReordered && $isMessagesAllowed) { + $this->eventMock + ->expects($this->atLeastOnce()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->orderMock->expects($this->once()) + ->method('getReordered') + ->willReturn($orderIsReordered); + $this->orderMock->expects($this->once()) + ->method('getGiftMessageId') + ->willReturn($giftMessageId); + $this->giftMessageMock->expects($this->once()) + ->method('isMessagesAllowed') + ->willReturn($isMessagesAllowed); + $this->messageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->messageMock); + $this->messageMock->expects($this->once()) + ->method('load') + ->with($giftMessageId) + ->willReturnSelf(); + $this->messageMock->expects($this->once()) + ->method('setId') + ->with(null) + ->willReturnSelf(); + $this->messageMock->expects($this->once()) + ->method('save') + ->willReturnSelf(); + $this->messageMock->expects($this->once()) + ->method('getId') + ->willReturn($newGiftMessageId); + $this->quoteMock->expects($this->once()) + ->method('setGiftMessageId') + ->with($newGiftMessageId) + ->willReturnSelf(); + } + + $this->observer->execute($this->observerMock); + } + + /** + * Providing gift message data + * + * @return array + */ + public function giftMessageDataProvider(): array + { + return [ + [false, true], + [true, true], + [false, true], + [false, false], + ]; + } +} diff --git a/app/code/Magento/GiftMessage/Ui/DataProvider/Product/Modifier/GiftMessage.php b/app/code/Magento/GiftMessage/Ui/DataProvider/Product/Modifier/GiftMessage.php index e3d617eac1cd2..fe2479d778992 100644 --- a/app/code/Magento/GiftMessage/Ui/DataProvider/Product/Modifier/GiftMessage.php +++ b/app/code/Magento/GiftMessage/Ui/DataProvider/Product/Modifier/GiftMessage.php @@ -53,7 +53,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -73,7 +73,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -101,24 +101,28 @@ protected function customizeAllowGiftMessageField(array $meta) 'children' ); $fieldPath = $this->arrayManager->findPath(static::FIELD_MESSAGE_AVAILABLE, $meta, null, 'children'); - $groupConfig = $this->arrayManager->get($containerPath, $meta); $fieldConfig = $this->arrayManager->get($fieldPath, $meta); - $meta = $this->arrayManager->merge($containerPath, $meta, [ - 'arguments' => [ - 'data' => [ - 'config' => [ - 'formElement' => 'container', - 'componentType' => 'container', - 'component' => 'Magento_Ui/js/form/components/group', - 'label' => $groupConfig['arguments']['data']['config']['label'], - 'breakLine' => false, - 'sortOrder' => $fieldConfig['arguments']['data']['config']['sortOrder'], - 'dataScope' => '', + $meta = $this->arrayManager->merge( + $containerPath, + $meta, + [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'formElement' => 'container', + 'componentType' => 'container', + 'component' => 'Magento_Ui/js/form/components/group', + 'label' => false, + 'required' => false, + 'breakLine' => false, + 'sortOrder' => $fieldConfig['arguments']['data']['config']['sortOrder'], + 'dataScope' => '', + ], ], ], - ], - ]); + ] + ); $meta = $this->arrayManager->merge( $containerPath, $meta, diff --git a/app/code/Magento/GiftMessage/composer.json b/app/code/Magento/GiftMessage/composer.json index 1aaad24837719..4d56514f365c1 100644 --- a/app/code/Magento/GiftMessage/composer.json +++ b/app/code/Magento/GiftMessage/composer.json @@ -17,6 +17,7 @@ "magento/module-ui": "*" }, "suggest": { + "magento/module-eav": "*", "magento/module-multishipping": "*" }, "type": "magento2-module", diff --git a/app/code/Magento/GiftMessage/etc/db_schema.xml b/app/code/Magento/GiftMessage/etc/db_schema.xml index 4ae98799df0c2..5c1e7fb17bc5d 100644 --- a/app/code/Magento/GiftMessage/etc/db_schema.xml +++ b/app/code/Magento/GiftMessage/etc/db_schema.xml @@ -9,9 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="gift_message" resource="default" engine="innodb" comment="Gift Message"> <column xsi:type="int" name="gift_message_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="GiftMessage Id"/> + comment="GiftMessage ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer id"/> + default="0" comment="Customer ID"/> <column xsi:type="varchar" name="sender" nullable="true" length="255" comment="Sender"/> <column xsi:type="varchar" name="recipient" nullable="true" length="255" comment="Registrant"/> <column xsi:type="text" name="message" nullable="true" comment="Message"/> @@ -21,27 +21,27 @@ </table> <table name="quote" resource="checkout" comment="Sales Flat Quote"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="quote_address" resource="checkout" comment="Sales Flat Quote Address"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="quote_item" resource="checkout" comment="Sales Flat Quote Item"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="quote_address_item" resource="checkout" comment="Sales Flat Quote Address Item"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="sales_order" resource="sales" comment="Sales Flat Order"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="sales_order_item" resource="sales" comment="Sales Flat Order Item"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> <column xsi:type="int" name="gift_message_available" padding="11" unsigned="false" nullable="true" identity="false" comment="Gift Message Available"/> </table> diff --git a/app/code/Magento/GiftMessage/etc/di.xml b/app/code/Magento/GiftMessage/etc/di.xml index 1d03849d978b8..5333084c90b75 100644 --- a/app/code/Magento/GiftMessage/etc/di.xml +++ b/app/code/Magento/GiftMessage/etc/di.xml @@ -28,4 +28,13 @@ <plugin name="save_gift_message" type="Magento\GiftMessage\Model\Plugin\OrderSave"/> <plugin name="get_gift_message" type="Magento\GiftMessage\Model\Plugin\OrderGet"/> </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="gift_message_available" xsi:type="string">catalog_product</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml index 76c377544cfb3..ed3dfcecb90f3 100644 --- a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml +++ b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml @@ -9,12 +9,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="googleoptimizer_code" resource="default" engine="innodb" comment="Google Experiment code"> <column xsi:type="int" name="code_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Google experiment code id"/> + comment="Google experiment code ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Optimized entity id product id or catalog id"/> + comment="Optimized entity ID product ID or catalog ID"/> <column xsi:type="varchar" name="entity_type" nullable="true" length="50" comment="Optimized entity type"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store id"/> + comment="Store ID"/> <column xsi:type="text" name="experiment_script" nullable="true" comment="Google experiment script"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="code_id"/> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index eb6a88a4d487d..559ccf9428929 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -36,10 +36,10 @@ directive @resolver(class: String="") on QUERY | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION - + directive @typeResolver(class: String="") on INTERFACE | OBJECT -directive @cache(cacheIdentity: String="" cachable: Boolean=true) on QUERY +directive @cache(cacheIdentity: String="" cacheable: Boolean=true) on QUERY type Query { } @@ -65,6 +65,20 @@ input FilterTypeInput @doc(description: "FilterTypeInput specifies which action nin: [String] @doc(description: "Not in. The value can contain a set of comma-separated values") } +input FilterEqualTypeInput @doc(description: "Defines a filter that matches the input exactly.") { + in: [String] @doc(description: "An array of values to filter on") + eq: String @doc(description: "A string to filter on") +} + +input FilterRangeTypeInput @doc(description: "Defines a filter that matches a range of values, such as prices or dates.") { + from: String @doc(description: "The beginning of the range") + to: String @doc(description: "The end of the range") +} + +input FilterMatchTypeInput @doc(description: "Defines a filter that performs a fuzzy search.") { + match: String @doc(description: "One or more words to filter on") +} + type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Plugin.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Plugin.php index 4777b2bae07a5..350edb5b8495e 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Plugin.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Plugin.php @@ -6,7 +6,7 @@ */ namespace Magento\GroupedProduct\Model\Product\Type; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; /** * Plugin. @@ -14,14 +14,14 @@ class Plugin { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager */ - public function __construct(ModuleManagerInterface $moduleManager) + public function __construct(Manager $moduleManager) { $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php index 519da20510815..c492939232b15 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php @@ -41,7 +41,7 @@ class AssociatedProductsCollection extends \Magento\Catalog\Model\ResourceModel\ * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -67,7 +67,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/StorefrontAddGroupedProductWithTwoLinksToCartActionGroup.xml b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/StorefrontAddGroupedProductWithTwoLinksToCartActionGroup.xml new file mode 100644 index 0000000000000..c1ba827f6ca8a --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/StorefrontAddGroupedProductWithTwoLinksToCartActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontAddGroupedProductWithTwoLinksToCartActionGroup" extends="AddSimpleProductToCart"> + <annotations> + <description>Adding to the Shopping Cart single Grouped product, with 2 associated from the Product page</description> + </annotations> + <arguments> + <argument name="linkedProduct1Name" type="string" defaultValue="{{_defaultProduct.name}}"/> + <argument name="linkedProduct2Name" type="string" defaultValue="{{_defaultProduct.name}}"/> + <argument name="linkedProduct1Qty" type="string" defaultValue="1"/> + <argument name="linkedProduct2Qty" type="string" defaultValue="1"/> + </arguments> + <fillField selector="{{StorefrontProductPageSection.qtyInputWithProduct(linkedProduct1Name)}}" userInput="{{linkedProduct1Qty}}" before="addToCart" stepKey="fillQuantityForFirsProduct"/> + <fillField selector="{{StorefrontProductPageSection.qtyInputWithProduct(linkedProduct2Name)}}" userInput="{{linkedProduct2Qty}}" after="fillQuantityForFirsProduct" stepKey="fillQuantityForSecondProduct"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml index ba3703e7b0edc..e6d7588289c39 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml @@ -28,6 +28,17 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiGroupedProductAndUnderscoredSku" type="product3"> + <data key="sku" unique="suffix">api_grouped_product</data> + <data key="type_id">grouped</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">Api Grouped Product</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-grouped-product</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> <entity name="ApiGroupedProduct2" type="product3"> <data key="sku" unique="suffix">apiGroupedProduct</data> <data key="type_id">grouped</data> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml index 5d65f82690235..f2cb2cc993a50 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml @@ -29,6 +29,9 @@ </createData> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteGroupedProduct"> + <argument name="sku" value="{{GroupedProduct.sku}}"/> + </actionGroup> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml new file mode 100644 index 0000000000000..3827666252478 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml @@ -0,0 +1,116 @@ +<?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="AdminAssociateGroupedProductToWebsitesTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Create/Edit grouped product in Admin"/> + <title value="Admin should be able to associate grouped product to websites"/> + <description value="Admin should be able to associate grouped product to websites"/> + <testCaseId value="MC-3377"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="groupedProduct"/> + </annotations> + + <before> + <!-- Set Store Code To Urls --> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToYes"/> + + <!-- Create grouped product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + + <!-- Reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindexAllIndexes"/> + </before> + + <after> + <!-- Disable Store Code To Urls --> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToNo"/> + + <!-- Delete product data --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + + <!-- Delete second website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + + <actionGroup ref="NavigateToAndResetProductGridToDefaultView" stepKey="resetProductGridFilter"/> + + <!-- Admin logout --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open product page and assign grouped project to second website --> + <actionGroup ref="filterAndSelectProduct" stepKey="openAdminProductPage"> + <argument name="productSku" value="$$createGroupedProduct.sku$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductInWebsiteActionGroup" stepKey="assignProductToSecondWebsite"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="AdminUnassignProductInWebsiteActionGroup" stepKey="unassignProductFromDefaultWebsite"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveGroupedProduct"/> + + <!-- Assert product is assigned to Second website --> + <actionGroup ref="AssertProductIsAssignedToWebsite" stepKey="seeCustomWebsiteIsChecked"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + + <!-- Assert product is not assigned to Main website --> + <actionGroup ref="AssertProductIsNotAssignedToWebsite" stepKey="seeMainWebsiteIsNotChecked"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + </actionGroup> + + <!-- Go to frontend and open product on Main website --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createGroupedProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Assert 404 page --> + <actionGroup ref="StorefrontAssertPageNotFoundErrorOnProductDetailPageActionGroup" stepKey="assertPageNotFoundErrorOnProductDetailPage"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + + <!-- Assert grouped product on Second website --> + <actionGroup ref="StorefrontOpenProductPageUsingStoreCodeInUrlActionGroup" stepKey="openProductPageUsingStoreCodeInUrl"> + <argument name="product" value="$$createGroupedProduct$$"/> + <argument name="storeView" value="SecondStoreUnique"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml index 2a600d38250f8..4fd06ccaa27ec 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml @@ -17,6 +17,42 @@ <severity value="MAJOR"/> <testCaseId value="MC-141"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByNameMysqlTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product name using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product name using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20464"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -51,7 +87,7 @@ <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> - <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="ApiGroupedProductAndUnderscoredSku" stepKey="product"/> <createData entity="OneSimpleProductLink" stepKey="addProductOne"> <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple1"/> @@ -77,6 +113,42 @@ <severity value="MAJOR"/> <testCaseId value="MC-282"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product description using the MYSQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20468"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -107,6 +179,42 @@ <severity value="MAJOR"/> <testCaseId value="MC-283"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByShortDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product short description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product short description using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20469"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -137,6 +245,51 @@ <severity value="MAJOR"/> <testCaseId value="MC-284"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <getData entity="GetProduct3" stepKey="arg1"> + <requiredEntity createDataKey="product"/> + </getData> + <getData entity="GetProduct" stepKey="arg2"> + <requiredEntity createDataKey="simple1"/> + </getData> + <getData entity="GetProduct" stepKey="arg3"> + <requiredEntity createDataKey="simple2"/> + </getData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByPriceMysqlTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product price using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product price using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20470"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest new file mode 100644 index 0000000000000..5220349a4aac3 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest @@ -0,0 +1,42 @@ +<?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="StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product sku that in camelCase format"/> + <description value="Guest customer should be able to advance search Grouped product with product sku that in camelCase format"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20519"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php index cec7931c1c61f..78fa2445ff583 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php @@ -30,7 +30,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManager; @@ -159,14 +159,9 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->categoryIndexerMock = $this->getMockForAbstractClass( - \Magento\Framework\Indexer\IndexerInterface::class - ); + $this->categoryIndexerMock = $this->getMockForAbstractClass(\Magento\Framework\Indexer\IndexerInterface::class); - $this->moduleManager = $this->createPartialMock( - \Magento\Framework\Module\ModuleManagerInterface::class, - ['isEnabled'] - ); + $this->moduleManager = $this->createPartialMock(\Magento\Framework\Module\Manager::class, ['isEnabled']); $this->stockItemFactoryMock = $this->createPartialMock( \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory::class, ['create'] diff --git a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..b2336a0741292 --- /dev/null +++ b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProductGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\Pricing\PriceInfoInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; + +/** + * Provides product prices for configurable products + */ +class Provider implements ProviderInterface +{ + /** + * Cache product prices so only fetch once + * + * @var AmountInterface[] + */ + private $minimalProductAmounts; + + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + return $this->getMinimalProductAmount($product, FinalPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + return $this->getMinimalProductAmount($product, RegularPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + //Use minimal for maximal since maximal price in infinite + return $this->getMinimalProductAmount($product, FinalPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + //Use minimal for maximal since maximal price in infinite + return $this->getMinimalProductAmount($product, RegularPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } + + /** + * Get minimal amount for cheapest product in group + * + * @param SaleableInterface $product + * @param string $priceType + * @return AmountInterface + */ + private function getMinimalProductAmount(SaleableInterface $product, string $priceType): AmountInterface + { + if (empty($this->minimalProductAmounts[$product->getId()][$priceType])) { + $products = $product->getTypeInstance()->getAssociatedProducts($product); + $minPrice = null; + foreach ($products as $item) { + $item->setQty(PriceInfoInterface::PRODUCT_QUANTITY_DEFAULT); + $price = $item->getPriceInfo()->getPrice($priceType); + $priceValue = $price->getValue(); + if (($priceValue !== false) && ($priceValue <= ($minPrice === null ? $priceValue : $minPrice))) { + $minPrice = $price->getValue(); + $this->minimalProductAmounts[$product->getId()][$priceType] = $price->getAmount(); + } + } + } + + return $this->minimalProductAmounts[$product->getId()][$priceType]; + } +} diff --git a/app/code/Magento/GroupedProductGraphQl/composer.json b/app/code/Magento/GroupedProductGraphQl/composer.json index cd22c6066eb4a..9578aa27ba180 100644 --- a/app/code/Magento/GroupedProductGraphQl/composer.json +++ b/app/code/Magento/GroupedProductGraphQl/composer.json @@ -5,6 +5,7 @@ "require": { "php": "~7.1.3||~7.2.0||~7.3.0", "magento/module-grouped-product": "*", + "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*", "magento/framework": "*" }, diff --git a/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml index 5c41fc26e615b..4c408b38b5ec3 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml @@ -29,4 +29,12 @@ </argument> </arguments> </type> + + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="grouped" xsi:type="object">Magento\GroupedProductGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php b/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php new file mode 100644 index 0000000000000..aa14d562d9cf7 --- /dev/null +++ b/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Api\Data; + +/** + * Extended export interface for implementation of Skipped Attributes which are missing from the basic interface + */ +interface ExtendedExportInfoInterface extends ExportInfoInterface +{ + /** + * Returns skipped attributes + * + * @return mixed + */ + public function getSkipAttr(); + + /** + * Set skipped attributes + * + * @param string $skipAttr + * @return mixed + */ + public function setSkipAttr($skipAttr); +} diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index d6b96a28afcc9..55992c92226af 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -5,6 +5,7 @@ */ namespace Magento\ImportExport\Block\Adminhtml\Import\Edit; +use Magento\Framework\App\ObjectManager; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; @@ -32,6 +33,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_behaviorFactory; + /** + * @var Import\ImageDirectoryBaseProvider + */ + private $imagesDirectoryProvider; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -40,6 +46,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\ImportExport\Model\Source\Import\EntityFactory $entityFactory * @param \Magento\ImportExport\Model\Source\Import\Behavior\Factory $behaviorFactory * @param array $data + * @param Import\ImageDirectoryBaseProvider|null $imageDirProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -48,12 +55,15 @@ public function __construct( \Magento\ImportExport\Model\Import $importModel, \Magento\ImportExport\Model\Source\Import\EntityFactory $entityFactory, \Magento\ImportExport\Model\Source\Import\Behavior\Factory $behaviorFactory, - array $data = [] + array $data = [], + ?Import\ImageDirectoryBaseProvider $imageDirProvider = null ) { $this->_entityFactory = $entityFactory; $this->_behaviorFactory = $behaviorFactory; parent::__construct($context, $registry, $formFactory, $data); $this->_importModel = $importModel; + $this->imagesDirectoryProvider = $imageDirProvider + ?? ObjectManager::getInstance()->get(Import\ImageDirectoryBaseProvider::class); } /** @@ -231,8 +241,15 @@ protected function _prepareForm() 'required' => false, 'class' => 'input-text', 'note' => __( - 'For Type "Local Server" use relative path to Magento installation, - e.g. var/export, var/import, var/export/some/dir' + $this->escapeHtml( + 'For Type "Local Server" use relative path to <Magento root directory>/' + .$this->imagesDirectoryProvider->getDirectoryRelativePath() + .', e.g. <i>product_images</i>, <i>import_images/batch1</i>.<br><br>' + .'For example, in case <i>product_images</i>, files should be placed into ' + .'<i><Magento root directory>/' + .$this->imagesDirectoryProvider->getDirectoryRelativePath() . '/product_images</i> folder.', + ['i', 'br'] + ) ), ] ); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php index 13c22a976e798..dff6560ebf768 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php @@ -3,15 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ImportExport\Controller\Adminhtml\Export; +use Magento\Backend\App\Action\Context; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\Response\Http\FileFactory; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\MessageQueue\PublisherInterface; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; -use Magento\Backend\App\Action\Context; -use Magento\Framework\App\Response\Http\FileFactory; use Magento\ImportExport\Model\Export as ExportModel; -use Magento\Framework\MessageQueue\PublisherInterface; use Magento\ImportExport\Model\Export\Entity\ExportInfoFactory; /** @@ -76,16 +78,24 @@ public function execute() try { $params = $this->getRequest()->getParams(); + if (!array_key_exists('skip_attr', $params)) { + $params['skip_attr'] = []; + } + /** @var ExportInfoFactory $dataObject */ $dataObject = $this->exportInfoFactory->create( $params['file_format'], $params['entity'], - $params['export_filter'] + $params['export_filter'], + $params['skip_attr'] ); $this->messagePublisher->publish('import_export.export', $dataObject); $this->messageManager->addSuccessMessage( - __('Message is added to queue, wait to get your file soon') + __( + 'Message is added to queue, wait to get your file soon.' + . ' Make sure your cron job is running to export the file' + ) ); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php index 10ae2dc5e58e1..1e9d194653c9c 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php @@ -67,6 +67,11 @@ public function execute() } $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); $path = $directory->getAbsolutePath() . 'export/' . $fileName; + + if (!$directory->isFile($path)) { + throw new LocalizedException(__('Sorry, but the data is invalid or the file is not uploaded.')); + } + $this->file->deleteFile($path); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php index e850f6af86cf9..5f036e51f66c4 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php @@ -6,12 +6,15 @@ namespace Magento\ImportExport\Controller\Adminhtml\Import; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\Framework\Controller\ResultFactory; use Magento\ImportExport\Model\Import; /** - * Controller responsible for initiating the import process + * Controller responsible for initiating the import process. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Start extends ImportResultController implements HttpPostActionInterface { @@ -25,25 +28,35 @@ class Start extends ImportResultController implements HttpPostActionInterface */ private $exceptionMessageFactory; + /** + * @var Import\ImageDirectoryBaseProvider + */ + private $imagesDirProvider; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor * @param \Magento\ImportExport\Model\History $historyModel * @param \Magento\ImportExport\Helper\Report $reportHelper - * @param \Magento\ImportExport\Model\Import $importModel + * @param Import $importModel * @param \Magento\Framework\Message\ExceptionMessageFactoryInterface $exceptionMessageFactory + * @param Import\ImageDirectoryBaseProvider|null $imageDirectoryBaseProvider */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor, \Magento\ImportExport\Model\History $historyModel, \Magento\ImportExport\Helper\Report $reportHelper, - \Magento\ImportExport\Model\Import $importModel, - \Magento\Framework\Message\ExceptionMessageFactoryInterface $exceptionMessageFactory + Import $importModel, + \Magento\Framework\Message\ExceptionMessageFactoryInterface $exceptionMessageFactory, + ?Import\ImageDirectoryBaseProvider $imageDirectoryBaseProvider = null ) { parent::__construct($context, $reportProcessor, $historyModel, $reportHelper); + $this->importModel = $importModel; $this->exceptionMessageFactory = $exceptionMessageFactory; + $this->imagesDirProvider = $imageDirectoryBaseProvider + ?? ObjectManager::getInstance()->get(Import\ImageDirectoryBaseProvider::class); } /** @@ -66,6 +79,8 @@ public function execute() ->addAction('hide', ['edit_form', 'upload_button', 'messages']); $this->importModel->setData($data); + //Images can be read only from given directory. + $this->importModel->setData('images_base_directory', $this->imagesDirProvider->getDirectory()); $errorAggregator = $this->importModel->getErrorAggregator(); $errorAggregator->initValidationStrategy( $this->importModel->getData(Import::FIELD_NAME_VALIDATION_STRATEGY), diff --git a/app/code/Magento/ImportExport/Files/Sample/catalog_product.csv b/app/code/Magento/ImportExport/Files/Sample/catalog_product.csv index 7ffd5b1dfb57c..c5d8df36b441c 100644 --- a/app/code/Magento/ImportExport/Files/Sample/catalog_product.csv +++ b/app/code/Magento/ImportExport/Files/Sample/catalog_product.csv @@ -1,7 +1,7 @@ -sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,crosssell_skus,upsell_skus,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus -24-WG085,,Default,simple,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap 6 foot,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and urable under strain.</p><ul><li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",14,,,,sprite-yoga-strap-6-foot,Meta Title,"meta1, meta2, meta3",meta description,"2015-10-25 03:34:20","2015-10-25 03:34:20",,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=1,quantity_and_stock_status=In Stock,required_options=0",100,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,"name=Custom Yoga Option,type=drop_down,required=0,price=10.0000,price_type=fixed,sku=,option_title=Gold|name=Custom Yoga Option,type=drop_down,required=0,price=10.0000,price_type=fixed,sku=,option_title=Silver|name=Custom Yoga Option,type=drop_down,required=0,price=10.0000,price_type=fixed,sku=yoga3sku,option_title=Platinum",,,,,, -24-WG086,,Default,simple,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap 8 foot,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>8' long x 1.0"" wide.<li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",17,,,,sprite-yoga-strap-8-foot,Meta Title,"meta1, meta2, meta4",meta description,"2015-10-25 03:34:20","2015-10-25 03:34:20",,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0",100,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,24-WG087,24-WG087,24-WG087,,,,,,,, -24-WG087,,Default,simple,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap 10 foot,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>10' long x 1.0"" wide.<li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",21,,,,sprite-yoga-strap-10-foot,Meta Title,"meta1, meta2, meta5",meta description,"2015-10-25 03:34:20","2015-10-25 03:34:20",,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0",100,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,24-WG086,24-WG086,24-WG086,,,,,,,, -24-WG085_Group,,Default,grouped,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Set of Sprite Yoga Straps,"<p>Great set of Sprite Yoga Straps for every stretch and hold you need. There are three straps in this set: 6', 8' and 10'.</p><ul><li>100% soft and durable cotton.</li><li>Plastic cinch buckle is easy to use.</li><li>Choice of three natural colors made from phthalate and heavy metal free dyes.</li></ul>",,,1,,"Catalog, Search",,,,,set-of-sprite-yoga-straps,Meta Title,"meta1, meta2, meta6",meta description,"2015-10-25 03:34:20","2015-10-25 03:34:20",,,Block after Info Column,,,,,,,,,,,,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0",0,0,1,0,0,1,1,0,0,1,1,,1,1,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,,,,,,,"24-WG085=5.0000,24-WG086=5.0000" -24-WG085-bundle-dynamic,,Default,bundle,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap Dynamic Bundle,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",14,,,,sprite-yoga-strap2,Meta Title,"meta1, meta2, meta8",meta description,"2015-10-25 03:34:20","2015-10-25 03:34:20",,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=1,shipment_type=together,quantity_and_stock_status=In Stock,required_options=0",0,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,,dynamic,dynamic,Price range,fixed,"name=Bundle Option One1,type=select,required=1,sku=24-WG085,price=15.0000,default=0,default_qty=1.0000,price_type=fixed|name=Bundle Option One1,type=select,required=1,sku=24-WG086,price=10.0000,default=1,default_qty=1.0000,price_type=fixed", -24-WG085-bundle-fixed,,Default,bundle,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap Fixed Bundle,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",14,,,,sprite-yoga-strap3,Meta Title,"meta1, meta2, meta9",meta description,"2015-10-25 03:34:20","2015-10-25 03:34:20",,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=1,shipment_type=together,quantity_and_stock_status=In Stock,required_options=0",0,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,,fixed,fixed,Price range,fixed,"name=Yoga Strap,type=radio,required=1,sku=24-WG086,price=0.0000,default=1,default_qty=3.0000,price_type=percent|name=Yoga Strap,type=radio,required=1,sku=24-WG085,price=0.0000,default=0,default_qty=3.0000,price_type=percent", \ No newline at end of file +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,crosssell_skus,upsell_skus,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +24-WG085,,Default,simple,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap 6 foot,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and urable under strain.</p><ul><li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1.23,1,Taxable Goods,"Catalog, Search",14,,,,sprite-yoga-strap-6-foot,Meta Title,"meta1, meta2, meta3",meta description,2015-10-25 3:34:20,2015-10-25 3:34:20,,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=1,quantity_and_stock_status=In Stock,required_options=0",100,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,"name=Custom Yoga Option,type=drop_down,required=0,price=10.0000,price_type=fixed,sku=,option_title=Gold|name=Custom Yoga Option,type=drop_down,required=0,price=10.0000,price_type=fixed,sku=,option_title=Silver|name=Custom Yoga Option,type=drop_down,required=0,price=10.0000,price_type=fixed,sku=yoga3sku,option_title=Platinum",,,,,, +24-WG086,,Default,simple,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap 8 foot,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>8' long x 1.0"" wide.<li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",17,,,,sprite-yoga-strap-8-foot,Meta Title,"meta1, meta2, meta4",meta description,2015-10-25 3:34:20,2015-10-25 3:34:20,,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0",100,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,24-WG087,24-WG087,24-WG087,,,,,,,, +24-WG087,,Default,simple,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap 10 foot,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>10' long x 1.0"" wide.<li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",21,,,,sprite-yoga-strap-10-foot,Meta Title,"meta1, meta2, meta5",meta description,2015-10-25 3:34:20,2015-10-25 3:34:20,,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0",100,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,24-WG086,24-WG086,24-WG086,,,,,,,, +24-WG085_Group,,Default,grouped,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Set of Sprite Yoga Straps,"<p>Great set of Sprite Yoga Straps for every stretch and hold you need. There are three straps in this set: 6', 8' and 10'.</p><ul><li>100% soft and durable cotton.</li><li>Plastic cinch buckle is easy to use.</li><li>Choice of three natural colors made from phthalate and heavy metal free dyes.</li></ul>",,,1,,"Catalog, Search",,,,,set-of-sprite-yoga-straps,Meta Title,"meta1, meta2, meta6",meta description,2015-10-25 3:34:20,2015-10-25 3:34:20,,,Block after Info Column,,,,,,,,,,,,,"has_options=0,quantity_and_stock_status=In Stock,required_options=0",0,0,1,0,0,1,1,0,0,1,1,,1,1,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,,,,,,,"24-WG085=5.0000,24-WG086=5.0000" +24-WG085-bundle-dynamic,,Default,bundle,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap Dynamic Bundle,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1.12,1,Taxable Goods,"Catalog, Search",14,,,,sprite-yoga-strap2,Meta Title,"meta1, meta2, meta8",meta description,2015-10-25 3:34:20,2015-10-25 3:34:20,,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=1,shipment_type=together,quantity_and_stock_status=In Stock,required_options=0",0,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,,dynamic,dynamic,Price range,fixed,"name=Bundle Option One1,type=select,required=1,sku=24-WG085,price=15.0000,default=0,default_qty=1.0000,price_type=fixed|name=Bundle Option One1,type=select,required=1,sku=24-WG086,price=10.0000,default=1,default_qty=1.0000,price_type=fixed", +24-WG085-bundle-fixed,,Default,bundle,"Default Category/Gear,Default Category/Gear/Fitness Equipment",base,Sprite Yoga Strap Fixed Bundle,"<p>The Sprite Yoga Strap is your untiring partner in demanding stretches, holds and alignment routines. The strap's 100% organic cotton fabric is woven tightly to form a soft, textured yet non-slip surface. The plastic clasp buckle is easily adjustable, lightweight and durable under strain.</p><ul><li>100% soft and durable cotton.<li>Plastic cinch buckle is easy to use.<li>Three natural colors made from phthalate and heavy metal free dyes.</ul>",,1,1,Taxable Goods,"Catalog, Search",14,,,,sprite-yoga-strap3,Meta Title,"meta1, meta2, meta9",meta description,2015-10-25 3:34:20,2015-10-25 3:34:20,,,Block after Info Column,,,,,,,,,,,Use config,,"has_options=1,shipment_type=together,quantity_and_stock_status=In Stock,required_options=0",0,0,1,0,0,1,1,0,0,1,1,,1,0,1,1,0,1,0,0,1,0,1,"24-WG087,24-WG086","24-WG087,24-WG086","24-WG087,24-WG086",,,fixed,fixed,Price range,fixed,"name=Yoga Strap,type=radio,required=1,sku=24-WG086,price=0.0000,default=1,default_qty=3.0000,price_type=percent|name=Yoga Strap,type=radio,required=1,sku=24-WG085,price=0.0000,default=0,default_qty=3.0000,price_type=percent", \ No newline at end of file diff --git a/app/code/Magento/ImportExport/Model/Export/Config/Converter.php b/app/code/Magento/ImportExport/Model/Export/Config/Converter.php index 20ab81ec1cd5b..298f63d18f88d 100644 --- a/app/code/Magento/ImportExport/Model/Export/Config/Converter.php +++ b/app/code/Magento/ImportExport/Model/Export/Config/Converter.php @@ -5,7 +5,7 @@ */ namespace Magento\ImportExport\Model\Export\Config; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\App\Utility\Classes; /** @@ -14,14 +14,14 @@ class Converter implements \Magento\Framework\Config\ConverterInterface { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** * @param Manager $moduleManager */ - public function __construct(ModuleManagerInterface $moduleManager) + public function __construct(Manager $moduleManager) { $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php index 6dffc1827cfd0..a5d5d63e4f8da 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php @@ -7,12 +7,12 @@ namespace Magento\ImportExport\Model\Export\Entity; -use \Magento\ImportExport\Api\Data\ExportInfoInterface; +use Magento\ImportExport\Api\Data\ExtendedExportInfoInterface; /** * Class ExportInfo implementation for ExportInfoInterface. */ -class ExportInfo implements ExportInfoInterface +class ExportInfo implements ExtendedExportInfoInterface { /** * @var string @@ -39,6 +39,11 @@ class ExportInfo implements ExportInfoInterface */ private $exportFilter; + /** + * @var mixed + */ + private $skipAttr; + /** * @inheritdoc */ @@ -118,4 +123,20 @@ public function setExportFilter($exportFilter) { $this->exportFilter = $exportFilter; } + + /** + * @inheritdoc + */ + public function getSkipAttr() + { + return $this->skipAttr; + } + + /** + * @inheritdoc + */ + public function setSkipAttr($skipAttr) + { + $this->skipAttr = $skipAttr; + } } diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php index e3cbd162aa5af..32c989acb661c 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php @@ -84,17 +84,25 @@ public function __construct( * @param string $fileFormat * @param string $entity * @param string $exportFilter + * @param array $skipAttr * @return ExportInfoInterface * @throws \Magento\Framework\Exception\LocalizedException */ - public function create($fileFormat, $entity, $exportFilter) + public function create($fileFormat, $entity, $exportFilter, $skipAttr) { $writer = $this->getWriter($fileFormat); - $entityAdapter = $this->getEntityAdapter($entity, $fileFormat, $exportFilter, $writer->getContentType()); + $entityAdapter = $this->getEntityAdapter( + $entity, + $fileFormat, + $exportFilter, + $skipAttr, + $writer->getContentType() + ); $fileName = $this->generateFileName($entity, $entityAdapter, $writer->getFileExtension()); /** @var ExportInfoInterface $exportInfo */ $exportInfo = $this->objectManager->create(ExportInfoInterface::class); $exportInfo->setExportFilter($this->serializer->serialize($exportFilter)); + $exportInfo->setSkipAttr($skipAttr); $exportInfo->setFileName($fileName); $exportInfo->setEntity($entity); $exportInfo->setFileFormat($fileFormat); @@ -130,11 +138,12 @@ private function generateFileName($entity, $entityAdapter, $fileExtensions) * @param string $entity * @param string $fileFormat * @param string $exportFilter + * @param array $skipAttr * @param string $contentType * @return \Magento\ImportExport\Model\Export\AbstractEntity|AbstractEntity * @throws \Magento\Framework\Exception\LocalizedException */ - private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentType) + private function getEntityAdapter($entity, $fileFormat, $exportFilter, $skipAttr, $contentType) { $entities = $this->exportConfig->getEntities(); if (isset($entities[$entity])) { @@ -166,12 +175,15 @@ private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentT } else { throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity.')); } - $entityAdapter->setParameters([ - 'fileFormat' => $fileFormat, - 'entity' => $entity, - 'exportFilter' => $exportFilter, - 'contentType' => $contentType, - ]); + $entityAdapter->setParameters( + [ + 'fileFormat' => $fileFormat, + 'entity' => $entity, + 'exportFilter' => $exportFilter, + 'skipAttr' => $skipAttr, + 'contentType' => $contentType, + ] + ); return $entityAdapter; } diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 04f4111d3a0a8..cf20001882c0d 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -13,6 +13,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; use Magento\Framework\HTTP\Adapter\FileTransferFactory; use Magento\Framework\Indexer\IndexerRegistry; @@ -107,7 +108,7 @@ class Import extends AbstractModel const DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR = ','; /** - * default empty attribute value constant + * Import empty attribute default value */ const DEFAULT_EMPTY_ATTRIBUTE_VALUE_CONSTANT = '__EMPTY__VALUE__'; const DEFAULT_SIZE = 50; @@ -464,8 +465,28 @@ public function importSource() { $this->setData('entity', $this->getDataSourceModel()->getEntityTypeCode()); $this->setData('behavior', $this->getDataSourceModel()->getBehavior()); - $this->importHistoryModel->updateReport($this); + //Validating images temporary directory path if the constraint has been provided + if ($this->hasData('images_base_directory') + && $this->getData('images_base_directory') instanceof Filesystem\Directory\ReadInterface + ) { + /** @var Filesystem\Directory\ReadInterface $imagesDirectory */ + $imagesDirectory = $this->getData('images_base_directory'); + if (!$imagesDirectory->isReadable()) { + $rootWrite = $this->_filesystem->getDirectoryWrite(DirectoryList::ROOT); + $rootWrite->create($imagesDirectory->getAbsolutePath()); + } + try { + $this->setData( + self::FIELD_NAME_IMG_FILE_DIR, + $imagesDirectory->getAbsolutePath($this->getData(self::FIELD_NAME_IMG_FILE_DIR)) + ); + $this->_getEntityAdapter()->setParameters($this->getData()); + } catch (ValidatorException $exception) { + throw new LocalizedException(__('Images file directory is outside required directory'), $exception); + } + } + $this->importHistoryModel->updateReport($this); $this->addLogComment(__('Begin import of "%1" with "%2" behavior', $this->getEntity(), $this->getBehavior())); $result = $this->processImport(); @@ -547,9 +568,15 @@ public function uploadSource() $entity = $this->getEntity(); /** @var $uploader Uploader */ $uploader = $this->_uploaderFactory->create(['fileId' => self::FIELD_NAME_SOURCE_FILE]); + $uploader->setAllowedExtensions(['csv', 'zip']); $uploader->skipDbProcessing(true); $fileName = $this->random->getRandomString(32) . '.' . $uploader->getFileExtension(); - $result = $uploader->save($this->getWorkingDir(), $fileName); + try { + $result = $uploader->save($this->getWorkingDir(), $fileName); + } catch (\Exception $e) { + throw new LocalizedException(__('The file cannot be uploaded.')); + } + // phpcs:disable Magento2.Functions.DiscouragedFunction.Discouraged $extension = pathinfo($result['file'], PATHINFO_EXTENSION); diff --git a/app/code/Magento/ImportExport/Model/Import/Config/Converter.php b/app/code/Magento/ImportExport/Model/Import/Config/Converter.php index f2d1596ec3d9d..a1eb8470b4dd6 100644 --- a/app/code/Magento/ImportExport/Model/Import/Config/Converter.php +++ b/app/code/Magento/ImportExport/Model/Import/Config/Converter.php @@ -5,7 +5,7 @@ */ namespace Magento\ImportExport\Model\Import\Config; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\App\Utility\Classes; /** @@ -14,14 +14,14 @@ class Converter implements \Magento\Framework\Config\ConverterInterface { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager */ - public function __construct(ModuleManagerInterface $moduleManager) + public function __construct(Manager $moduleManager) { $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/ImportExport/Model/Import/ImageDirectoryBaseProvider.php b/app/code/Magento/ImportExport/Model/Import/ImageDirectoryBaseProvider.php new file mode 100644 index 0000000000000..9c90b57c30ea8 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/ImageDirectoryBaseProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Import; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\App\Filesystem\DirectoryList; + +/** + * Provides base directory to use for images when user imports entities. + */ +class ImageDirectoryBaseProvider +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param ScopeConfigInterface $config + * @param Filesystem $filesystem + */ + public function __construct(ScopeConfigInterface $config, Filesystem $filesystem) + { + $this->config = $config; + $this->filesystem = $filesystem; + } + + /** + * Directory that users are allowed to place images for importing. + * + * @return ReadInterface + */ + public function getDirectory(): ReadInterface + { + $path = $this->getDirectoryRelativePath(); + + return $this->filesystem->getDirectoryReadByPath( + $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath($path) + ); + } + + /** + * The directory's path relative to Magento root. + * + * @return string + */ + public function getDirectoryRelativePath(): string + { + return $this->config->getValue('general/file/import_images_base_dir'); + } +} diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php index 6fa87ab5d5c4d..7e69d837d6526 100644 --- a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php +++ b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php @@ -33,6 +33,12 @@ public function __construct( throw new ValidatorException(__('Sorry, but the data is invalid or the file is not uploaded.')); } $directory->delete($directory->getRelativePath($file)); - parent::__construct($csvFile, $directory, $options); + + try { + parent::__construct($csvFile, $directory, $options); + } catch (\LogicException $e) { + $directory->delete($directory->getRelativePath($csvFile)); + throw $e; + } } } diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminAssertVisiblePagerActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminAssertVisiblePagerActionGroup.xml new file mode 100644 index 0000000000000..298be79a6ac9f --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminAssertVisiblePagerActionGroup.xml @@ -0,0 +1,15 @@ +<?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="AdminAssertVisiblePagerActionGroup"> + <seeElement selector="{{AdminExportGridPagerSection.pager}}" stepKey="seeGridPager"/> + <seeElement selector="{{AdminGridRowsPerPage.count}}" stepKey="seeCountPerPageElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminCheckDataForImportProductActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminCheckDataForImportProductActionGroup.xml new file mode 100644 index 0000000000000..8628d246248b1 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminCheckDataForImportProductActionGroup.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="AdminCheckDataForImportProductActionGroup"> + <arguments> + <argument name="behavior" type="string" defaultValue="Add/Update"/> + <argument name="importFile" type="string"/> + </arguments> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <waitForPageLoad stepKey="adminImportMainSectionLoad"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportBehaviorOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="{{importFile}}" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml index 2ac790b953ec1..956822fc3cbef 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml @@ -14,21 +14,26 @@ </annotations> <arguments> <argument name="behavior" type="string"/> + <argument name="validationStrategy" type="string" defaultValue="Stop on Error"/> + <argument name="allowedErrorsCount" type="string" defaultValue="10"/> <argument name="importFile" type="string"/> - <argument name="importMessage" type="string"/> + <argument name="importNoticeMessage" type="string" defaultValue=""/> + <argument name="importMessageType" type="string" defaultValue="success"/> + <argument name="importMessage" type="string" defaultValue="Import successfully done"/> </arguments> <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> - <waitForPageLoad stepKey="AdminImportMainSectionLoad"/> + <waitForPageLoad stepKey="adminImportMainSectionLoad"/> <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportOption"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportBehaviorOption"/> + <selectOption selector="{{AdminImportMainSection.validationStrategy}}" userInput="{{validationStrategy}}" stepKey="selectValidationStrategyOption"/> + <fillField selector="{{AdminImportMainSection.allowedErrorsCount}}" userInput="{{allowedErrorsCount}}" stepKey="fillAllowedErrorsCountField"/> <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="{{importFile}}" stepKey="attachFileForImport"/> <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <waitForPageLoad stepKey="AdminImportMainSectionLoad2"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> - <waitForPageLoad stepKey="AdminMessagesSection"/> - <see selector="{{AdminMessagesSection.notice}}" userInput="{{importMessage}}" stepKey="seeImportMessage"/> + <waitForElementVisible selector="{{AdminImportValidationMessagesSection.notice}}" stepKey="waitForNoticeMessage"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="{{importNoticeMessage}}" stepKey="seeNoticeMessage"/> + <see selector="{{AdminImportValidationMessagesSection.messageByType(importMessageType)}}" userInput="{{importMessage}}" stepKey="seeImportMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsWithCheckValidationResultActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsWithCheckValidationResultActionGroup.xml new file mode 100644 index 0000000000000..a631ec72a5a72 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsWithCheckValidationResultActionGroup.xml @@ -0,0 +1,20 @@ +<?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="AdminImportProductsWithCheckValidationResultActionGroup" extends="AdminImportProductsActionGroup"> + <arguments> + <argument name="validationNoticeMessage" type="string"/> + <argument name="validationMessage" type="string" defaultValue="File is valid! To start import process press "Import" button"/> + </arguments> + <waitForElementVisible selector="{{AdminImportValidationMessagesSection.notice}}" after="clickCheckDataButton" stepKey="waitForValidationNoticeMessage"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="{{validationNoticeMessage}}" after="waitForValidationNoticeMessage" stepKey="seeValidationNoticeMessage"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="{{validationMessage}}" after="seeValidationNoticeMessage" stepKey="seeValidationMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminNavigateToExportPageActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminNavigateToExportPageActionGroup.xml new file mode 100644 index 0000000000000..1c3c241c7b0fa --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminNavigateToExportPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="AdminNavigateToExportPageActionGroup"> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="navigateToExportPage"/> + <waitForPageLoad stepKey="waitForExportPageOpened"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml index 55ed3edd9bc79..3d0aff2e113ae 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminExportIndexPage" url="admin/export/" area="admin" module="Magento_ImportExport"> <section name="AdminExportMainSection"/> + <section name="AdminExportGridPagerSection"/> </page> </pages> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml index 87807eb9b0e85..6262931179599 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml @@ -11,5 +11,6 @@ <page name="AdminImportIndexPage" url="admin/import/" area="admin" module="Magento_ImportExport"> <section name="AdminImportHeaderSection"/> <section name="AdminImportMainSection"/> + <section name="AdminImportValidationMessagesSection"/> </page> </pages> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml index 528ad23aaf2bf..f9b07a59c8763 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml @@ -17,5 +17,6 @@ <element name="selectByIndex" type="button" selector="//tr[@data-repeat-index='{{var}}']//button" parameterized="true" timeout="30"/> <element name="download" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Download']" parameterized="true" timeout="30"/> <element name="delete" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Delete']" parameterized="true" timeout="30"/> + <element name="exportFileNameByPosition" type="text" selector="[data-role='grid'] tr[data-repeat-index='{{position}}'] div.data-grid-cell-content" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportGridPagerSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportGridPagerSection.xml new file mode 100644 index 0000000000000..8c4615c5926f0 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportGridPagerSection.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="AdminExportGridPagerSection"> + <element name="pager" type="text" selector=".admin__data-grid-header div.admin__data-grid-pager-wrap"/> + <element name="recordsLabel" type="text" selector=".admin__data-grid-header .admin__control-support-text"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml index 748580be09406..c39ebbe04f2e1 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminImportHeaderSection"> <element name="checkDataButton" type="button" selector="#upload_button" timeout="30"/> + <element name="messageNote" type="text" selector="#import_file-note" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml index 2ce6b1e35777f..ba1deeebbd89a 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml @@ -13,5 +13,9 @@ <element name="importBehavior" type="select" selector="#basic_behavior"/> <element name="selectFileToImport" type="input" selector="#import_file"/> <element name="importButton" type="button" selector="#import_validation_container button" timeout="30"/> + <element name="messageSuccess" type="text" selector=".messages div.message-success"/> + <element name="messageError" type="text" selector=".messages div.message-error"/> + <element name="validationStrategy" type="select" selector="#basic_behaviorvalidation_strategy"/> + <element name="allowedErrorsCount" type="input" selector="#basic_behavior_allowed_error_count"/> </section> </sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml new file mode 100644 index 0000000000000..370d9546fa2f7 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.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="AdminImportValidationMessagesSection"> + <element name="notice" type="text" selector="#import_validation_messages .message-notice"/> + <element name="success" type="text" selector="#import_validation_messages .message-success"/> + <element name="messageByType" type="text" selector="#import_validation_messages .message-{{messageType}}" parameterized="true" /> + <element name="importErrorList" type="text" selector="#import_validation_messages .import-error-list"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml index 0f2dde99b9016..909c6101fe53e 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml @@ -60,14 +60,14 @@ <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProductsFirstTime"> <argument name="behavior" value="Add/Update"/> <argument name="importFile" value="prepared-for-sample-data.csv"/> - <argument name="importMessage" value="Created: 100, Updated: 3, Deleted: 0"/> + <argument name="importNoticeMessage" value="Created: 100, Updated: 3, Deleted: 0"/> </actionGroup> <!-- Import products with add/update behavior again --> <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProductsSecondTime"> <argument name="behavior" value="Add/Update"/> <argument name="importFile" value="prepared-for-sample-data.csv"/> - <argument name="importMessage" value="Created: 0, Updated: 300, Deleted: 0"/> + <argument name="importNoticeMessage" value="Created: 0, Updated: 300, Deleted: 0"/> </actionGroup> </test> </tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml new file mode 100644 index 0000000000000..42516e5a5a363 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml @@ -0,0 +1,77 @@ +<?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="AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest"> + <annotations> + <features value="Import/Export"/> + <stories value="Attribute importing"/> + <title value="Check that some attributes changed the value to an empty after import CSV"/> + <description value="Check that some attributes changed the value to an empty after import CSV"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11332"/> + <useCaseId value="MAGETWO-61593"/> + <group value="importExport"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndexPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridToDefaultView"/> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> + <createData entity="productDropDownAttribute" stepKey="productAttribute"/> + <createData entity="productAttributeOption2" stepKey="attributeOptionWithDefaultValue"> + <requiredEntity createDataKey="productAttribute"/> + </createData> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + </before> + <after> + <!--Delete Product and Category--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> + <argument name="productName" value="simpleProductWithShortNameAndSku.name"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete attribute--> + <deleteData createDataKey="productAttribute" stepKey="deleteProductAttribute"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Create product--> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductFieldsInAdmin"> + <argument name="product" value="simpleProductWithShortNameAndSku"/> + </actionGroup> + <actionGroup ref="SetCategoryByName" stepKey="addCategoryToProduct"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <!--Select created attribute--> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="$$productAttribute.attribute_code$$"/> + </actionGroup> + <!--Check that attribute value is selected--> + <scrollTo selector="{{AdminProductFormSection.attributeTab}}" stepKey="scrollToAttributeTitle1"/> + <conditionalClick selector="{{AdminProductFormSection.attributeTab}}" dependentSelector="{{AdminProductAttributeSection.dropDownAttribute($$productAttribute.attribute_code$$)}}" visible="false" stepKey="expandAttributeTab1"/> + <seeOptionIsSelected selector="{{AdminProductAttributeSection.dropDownAttribute($$productAttribute.attribute_code$$)}}" userInput="option2" stepKey="seeAttributeValueIsSelected1"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Import product with add/update behavior--> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProductsFirstTime"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_simple_product.csv"/> + <argument name="importNoticeMessage" value="Created: 0, Updated: 1, Deleted: 0"/> + </actionGroup> + <!--Check that attribute value is empty after import--> + <actionGroup ref="filterAndSelectProduct" stepKey="filterAndSelectTheProduct2"> + <argument name="productSku" value="{{simpleProductWithShortNameAndSku.sku}}"/> + </actionGroup> + <scrollTo selector="{{AdminProductFormSection.attributeTab}}" stepKey="scrollToAttributeTitle2"/> + <conditionalClick selector="{{AdminProductFormSection.attributeTab}}" dependentSelector="{{AdminProductAttributeSection.dropDownAttribute($$productAttribute.attribute_code$$)}}" visible="false" stepKey="expandAttributeTab2"/> + <seeOptionIsSelected selector="{{AdminProductAttributeSection.dropDownAttribute($$productAttribute.attribute_code$$)}}" userInput="" stepKey="seeAttributeValueIsSelected2"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml new file mode 100644 index 0000000000000..b203a4d11a2d2 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml @@ -0,0 +1,29 @@ +<?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="AdminExportPagerGridTest"> + <annotations> + <features value="ImportExport"/> + <stories value="Export Grid"/> + <title value="Testing if the grid is present on export page"/> + <description value="Admin should be able to see the grid onn export page"/> + <severity value="CRITICAL"/> + <group value="importExport"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateToExportPageActionGroup" stepKey="navigateToExportPage"/> + <actionGroup ref="AdminAssertVisiblePagerActionGroup" stepKey="seeGridPager"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml new file mode 100644 index 0000000000000..38c1a09dc534c --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml @@ -0,0 +1,35 @@ +<?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="AdminImportCSVWithSpecialCharactersTest"> + <annotations> + <features value="Import/Export"/> + <stories value="Import CSV file"/> + <title value="Import CSV with special characters"/> + <description value="Import CSV with special characters"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6416"/> + <useCaseId value="MAGETWO-91569"/> + <group value="importExport"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <actionGroup ref="AdminCheckDataForImportProductActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="importSpecChars.csv"/> + </actionGroup> + <see selector="{{AdminImportHeaderSection.messageNote}}" userInput='File must be saved in UTF-8 encoding for proper import' stepKey="seeNoteMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput='File is valid! To start import process press "Import" button' stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml index ceb4e93e4e9aa..796732d572290 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -72,7 +72,7 @@ <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> <argument name="behavior" value="Add/Update"/> <argument name="importFile" value="catalog_import_products.csv"/> - <argument name="importMessage" value="Created: 2, Updated: 1, Deleted: 0"/> + <argument name="importNoticeMessage" value="Created: 2, Updated: 1, Deleted: 0"/> </actionGroup> <!-- Assert Simple Product1 on grid--> @@ -109,7 +109,7 @@ <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct1Validation"> <argument name="product" value="SimpleProductAfterImport1"/> </actionGroup> - + <!-- Assert SimpleProduct2 on store front--> <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct2Validation"> <argument name="product" value="SimpleProductAfterImport2"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml index 4cbb0603d9073..7ec48a3a7e8fd 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml @@ -47,8 +47,8 @@ <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="catalog_products.csv" stepKey="attachFileForImport"/> <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> - <see selector="{{AdminMessagesSection.notice}}" userInput="Created: 0, Updated: 0, Deleted: 3" stepKey="assertNotice"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 0, Updated: 0, Deleted: 3" stepKey="assertNotice"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchSimpleProductOnBackend"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml new file mode 100644 index 0000000000000..94840a4ea6142 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.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="AdminImportProductsWithErrorEntriesTest"> + <annotations> + <features value="ImportExport"/> + <stories value="Import Products"/> + <title value="Import products with error entries"/> + <description value="Verify import status during import products with error entries"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6358"/> + <useCaseId value="MAGETWO-65066"/> + <group value="importExport"/> + </annotations> + <before> + <!--Login to Admin Page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Clear products grid filters--> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <!--Delete all imported products--> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> + <!--Logout from Admin page--> + <actionGroup ref="logout" stepKey="logoutFromAdminPage"/> + </after> + + <!--Import products with "Skip error entries"--> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProductsWithSkipErrorEntries"> + <argument name="behavior" value="Add/Update"/> + <argument name="validationStrategy" value="Skip error entries"/> + <argument name="importFile" value="catalog_product_err_img.csv"/> + <argument name="importNoticeMessage" value="Created: 10, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 10, checked entities: 10, invalid rows: 0, total errors: 0"/> + </actionGroup> + <see selector="{{AdminImportValidationMessagesSection.importErrorList}}" userInput="row(s): 1, 2, 3, 4, 5, 6, 7, 8, 9, 10" stepKey="seeTenImportError"/> + + <!--Import products with "Stop on Error" and "Allowed Errors Count" equals 5--> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProductsWithAllowedErrorsCountFive"> + <argument name="behavior" value="Add/Update"/> + <argument name="allowedErrorsCount" value="5"/> + <argument name="importFile" value="catalog_product_err_img.csv"/> + <argument name="importNoticeMessage" value="Following Error(s) has been occurred during importing process"/> + <argument name="importMessageType" value="error"/> + <argument name="importMessage" value="Maximum error count has been reached or system error is occurred!"/> + <argument name="validationNoticeMessage" value="Checked rows: 10, checked entities: 10, invalid rows: 0, total errors: 0"/> + </actionGroup> + <see selector="{{AdminImportValidationMessagesSection.importErrorList}}" userInput="row(s): 1, 2, 3, 4, 5, 6" stepKey="seeAboutFiveImportError"/> + + <!--Import products with "Stop on Error" and "Allowed Errors Count" equals 11--> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProductsWithAllowedErrorsCountEleven"> + <argument name="behavior" value="Add/Update"/> + <argument name="allowedErrorsCount" value="11"/> + <argument name="importFile" value="catalog_product_err_img.csv"/> + <argument name="importNoticeMessage" value="Created: 0, Updated: 10, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 10, checked entities: 10, invalid rows: 0, total errors: 0"/> + </actionGroup> + <see selector="{{AdminImportValidationMessagesSection.importErrorList}}" userInput="row(s): 1, 2, 3, 4, 5, 6, 7, 8, 9, 10" stepKey="seeAboutTenImportError"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml index d63a5546716b1..dc4ede1978de3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml @@ -39,7 +39,7 @@ <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> <argument name="behavior" value="Replace"/> <argument name="importFile" value="catalog_import_products.csv"/> - <argument name="importMessage" value="Created: 3, Updated: 0, Deleted: 3"/> + <argument name="importNoticeMessage" value="Created: 3, Updated: 0, Deleted: 3"/> </actionGroup> </test> </tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml new file mode 100644 index 0000000000000..56c1c43bc28d2 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml @@ -0,0 +1,42 @@ +<?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="AdminProductImportCSVFileCorrectDifferentFilesTest"> + <annotations> + <description value="Product import from CSV file correct from different files."/> + <features value="Import/Export"/> + <stories value="Product Import"/> + <title value="Product import from CSV file correct from different files."/> + <severity value="MAJOR"/> + <testCaseId value="MC-17104"/> + <useCaseId value="MAGETWO-70803"/> + <group value="importExport"/> + </annotations> + <before> + <!--Login as Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Logout from Admin--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Check data products with add/update behavior--> + <actionGroup ref="AdminCheckDataForImportProductActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="BB-ProductsWorking.csv"/> + </actionGroup> + <see selector="{{AdminImportMainSection.messageSuccess}}" userInput='File is valid! To start import process press "Import" button' stepKey="seeSuccessMessage"/> + <actionGroup ref="AdminCheckDataForImportProductActionGroup" stepKey="adminImportProducts1"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="BB-Products.csv"/> + </actionGroup> + <see selector="{{AdminImportMainSection.messageError}}" userInput='Curly quotes used instead of straight quotes in row(s): 84, 85' stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml new file mode 100644 index 0000000000000..0a1423ece71e0 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml @@ -0,0 +1,73 @@ +<?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="AdminProductVisibilityDifferentStoreViewsAfterImportTest"> + <annotations> + <features value="Import/Export"/> + <stories value="Import Products"/> + <title value="Checking product visibility in different store views after product importing"/> + <description value="Checking product visibility in different store views after product importing"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6406"/> + <useCaseId value="MAGETWO-59265"/> + <group value="importExport"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create English and Chinese store views--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createEnglishStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createChineseStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="storeViewChinese"/> + </actionGroup> + </before> + <after> + <!--Delete all imported products--> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="adminDataGridSelectPerPage" stepKey="selectNumberOfProductsPerPage"> + <argument name="perPage" value="100"/> + </actionGroup> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> + <!--Delete store views--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteEnglishStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteChineseStoreView"> + <argument name="customStore" value="storeViewChinese"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Import products from file--> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="importProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_productsoftwostoresdata.csv"/> + <argument name="importNoticeMessage" value="Created: 2, Updated: 0, Deleted: 0"/> + </actionGroup> + <!--Open imported name4 product--> + <actionGroup ref="filterAndSelectProduct" stepKey="openName4Product"> + <argument name="productSku" value="name4"/> + </actionGroup> + <!--Switch Chinese store view and assert visibility field--> + <comment userInput="Switch Chinese store view and assert visibility field" stepKey="commentAssertVisibilityChineseView"/> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> + <argument name="storeViewName" value="{{storeViewChinese.name}}"/> + </actionGroup> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="Catalog" stepKey="seeVisibilityFieldForChineseStore"/> + <!--Switch English store view and assert visibility field--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomEnglishView"> + <argument name="storeViewName" value="{{customStoreEN.name}}"/> + </actionGroup> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="Catalog" stepKey="seeVisibilityFieldForEnglishView"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml new file mode 100644 index 0000000000000..8d56d9d8dad9d --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml @@ -0,0 +1,49 @@ +<?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="AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest"> + <annotations> + <features value="Import/Export"/> + <stories value="Import Products"/> + <title value="Check that new URL Key works after updating a product through importing CSV file"/> + <description value="Check that new URL Key works after updating a product through importing CSV file"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6317"/> + <useCaseId value="MAGETWO-91544"/> + <group value="importExport"/> + </annotations> + <before> + <!--Create Product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProductBeforeUpdate" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Import product from CSV file--> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="simpleProductUpdate.csv"/> + <argument name="importNoticeMessage" value="Created: 0, Updated: 1, Deleted: 0"/> + </actionGroup> + <!--Assert product's updated url--> + <amOnPage url="{{StorefrontProductPage.url('simpleprod')}}" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url('simpleprod')}}" stepKey="seeUpdatedUrl"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="assertProductName"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createProduct.sku$$" stepKey="assertProductSku"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Helper/DataTest.php b/app/code/Magento/ImportExport/Test/Unit/Helper/DataTest.php new file mode 100644 index 0000000000000..85630d2106b45 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Helper/DataTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Helper; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\File\Size as FileSize; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\ImportExport\Helper\Data as HelperData; +use PHPUnit\Framework\TestCase; + +/** + * Test class to cover Data Helper + * + * Class \Magento\ImportExport\Test\Unit\Helper\DataTest + */ +class DataTest extends TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var FileSize|PHPUnit_Framework_MockObject_MockObject + */ + private $fileSizeMock; + + /** + * @var Context|PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var ScopeConfigInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var HelperData|PHPUnit_Framework_MockObject_MockObject + */ + private $helperData; + + /** + * Set up environment + */ + protected function setUp() + { + $this->contextMock = $this->createMock(Context::class); + $this->fileSizeMock = $this->createMock(FileSize::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->contextMock->expects($this->any())->method('getScopeConfig')->willReturn($this->scopeConfigMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->helperData = $this->objectManagerHelper->getObject( + HelperData::class, + [ + 'context' => $this->contextMock, + 'fileSize' => $this->fileSizeMock + ] + ); + } + + /** + * Test getMaxUploadSizeMessage() with data provider below + * + * @param float $maxImageSize + * @param string $expected + * @return void + * @dataProvider getMaxUploadSizeMessageDataProvider + */ + public function testGetMaxUploadSizeMessage($maxImageSize, $expected) + { + $this->fileSizeMock->expects($this->any())->method('getMaxFileSizeInMb')->willReturn($maxImageSize); + $this->assertEquals($expected, $this->helperData->getMaxUploadSizeMessage()); + } + + /** + * DataProvider for testGetMaxUploadSizeMessage() function + * + * @return array + */ + public function getMaxUploadSizeMessageDataProvider() + { + return [ + 'Test with max image size = 10Mb' => [ + 'maxImageSize' => 10, + 'expected' => 'Make sure your file isn\'t more than 10M.', + ], + 'Test with max image size = 0' => [ + 'maxImageSize' => 0, + 'expected' => 'We can\'t provide the upload settings right now.', + ] + ]; + } + + /** + * Test getLocalValidPaths() + * + * @return void + */ + public function testGetLocalValidPaths() + { + $paths = [ + 'available' => [ + 'export_xml' => 'var/export/*/*.xml', + 'export_csv' => 'var/export/*/*.csv', + 'import_xml' => 'var/import/*/*.xml', + 'import_csv' => 'var/import/*/*.csv', + ] + ]; + $this->scopeConfigMock->expects($this->any())->method('getValue') + ->with(HelperData::XML_PATH_EXPORT_LOCAL_VALID_PATH) + ->willReturn($paths); + + $this->assertEquals($paths, $this->helperData->getLocalValidPaths()); + } + + /** + * Test getBunchSize() + * + * @return void + */ + public function testGetBunchSize() + { + $bunchSize = '100'; + + $this->scopeConfigMock->expects($this->any())->method('getValue') + ->with(HelperData::XML_PATH_BUNCH_SIZE) + ->willReturn($bunchSize); + + $this->assertEquals(100, $this->helperData->getBunchSize()); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/ConverterTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/ConverterTest.php index c888c6b447348..e08f382d94003 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/ConverterTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/ConverterTest.php @@ -21,7 +21,7 @@ class ConverterTest extends \PHPUnit\Framework\TestCase protected $filePath; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManager; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/ConverterTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/ConverterTest.php index b29a04322ce4f..69118d2e2a319 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/ConverterTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/ConverterTest.php @@ -21,7 +21,7 @@ class ConverterTest extends \PHPUnit\Framework\TestCase protected $filePath; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManager; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php index 50e71512c3d28..04a15179477d8 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php @@ -640,6 +640,9 @@ public function testGetUnknownEntity($entity) $import->getEntity(); } + /** + * @return array + */ public function unknownEntitiesProvider() { return [ diff --git a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php index e64a6df430ea1..83203f1ad8aff 100644 --- a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php +++ b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php @@ -7,16 +7,23 @@ namespace Magento\ImportExport\Ui\DataProvider; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File; /** * Data provider for export grid. */ class ExportFileDataProvider extends DataProvider { + /** + * @var File|null + */ + private $fileIO; + /** * @var DriverInterface */ @@ -37,6 +44,7 @@ class ExportFileDataProvider extends DataProvider * @param \Magento\Framework\Api\FilterBuilder $filterBuilder * @param DriverInterface $file * @param Filesystem $filesystem + * @param File|null $fileIO * @param array $meta * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -51,6 +59,7 @@ public function __construct( \Magento\Framework\Api\FilterBuilder $filterBuilder, DriverInterface $file, Filesystem $filesystem, + File $fileIO = null, array $meta = [], array $data = [] ) { @@ -67,6 +76,8 @@ public function __construct( $meta, $data ); + + $this->fileIO = $fileIO ?: ObjectManager::getInstance()->get(File::class); } /** @@ -83,17 +94,45 @@ public function getData() return $emptyResponse; } - $files = $this->file->readDirectoryRecursively($directory->getAbsolutePath() . 'export/'); + $files = $this->getExportFiles($directory->getAbsolutePath() . 'export/'); if (empty($files)) { return $emptyResponse; } $result = []; foreach ($files as $file) { - $result['items'][]['file_name'] = basename($file); + $result['items'][]['file_name'] = $this->fileIO->getPathInfo($file)['basename']; } + $pageSize = (int) $this->request->getParam('paging')['pageSize']; + $pageCurrent = (int) $this->request->getParam('paging')['current']; + $pageOffset = ($pageCurrent - 1) * $pageSize; $result['totalRecords'] = count($result['items']); + $result['items'] = array_slice($result['items'], $pageOffset, $pageSize); return $result; } + + /** + * Get files from directory path, sort them by date modified and return sorted array of full path to files + * + * @param string $directoryPath + * @return array + * @throws \Magento\Framework\Exception\FileSystemException + */ + private function getExportFiles(string $directoryPath): array + { + $sortedFiles = []; + $files = $this->file->readDirectoryRecursively($directoryPath); + if (empty($files)) { + return []; + } + foreach ($files as $filePath) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $sortedFiles[filemtime($filePath)] = $filePath; + } + //sort array elements using key value + krsort($sortedFiles); + + return $sortedFiles; + } } diff --git a/app/code/Magento/ImportExport/etc/communication.xml b/app/code/Magento/ImportExport/etc/communication.xml index 7794b3e5ab248..3f87eef1ddbd4 100644 --- a/app/code/Magento/ImportExport/etc/communication.xml +++ b/app/code/Magento/ImportExport/etc/communication.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> - <topic name="import_export.export" request="Magento\ImportExport\Api\Data\ExportInfoInterface"> + <topic name="import_export.export" request="Magento\ImportExport\Api\Data\ExtendedExportInfoInterface"> <handler name="exportProcessor" type="Magento\ImportExport\Model\Export\Consumer" method="process" /> </topic> </config> diff --git a/app/code/Magento/ImportExport/etc/config.xml b/app/code/Magento/ImportExport/etc/config.xml index 7aee9bdd2fd6d..b8ce1c70ee16d 100644 --- a/app/code/Magento/ImportExport/etc/config.xml +++ b/app/code/Magento/ImportExport/etc/config.xml @@ -18,6 +18,7 @@ </available> </importexport_local_valid_paths> <bunch_size>100</bunch_size> + <import_images_base_dir>var/import/images</import_images_base_dir> </file> </general> <import> diff --git a/app/code/Magento/ImportExport/etc/db_schema.xml b/app/code/Magento/ImportExport/etc/db_schema.xml index df45131848519..404999cb9e07a 100644 --- a/app/code/Magento/ImportExport/etc/db_schema.xml +++ b/app/code/Magento/ImportExport/etc/db_schema.xml @@ -8,7 +8,7 @@ <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="importexport_importdata" resource="default" engine="innodb" comment="Import Data Table"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="varchar" name="entity" nullable="false" length="50" comment="Entity"/> <column xsi:type="varchar" name="behavior" nullable="false" length="10" default="append" comment="Behavior"/> <column xsi:type="longtext" name="data" nullable="true" comment="Data"/> @@ -18,7 +18,7 @@ </table> <table name="import_history" resource="default" engine="innodb" comment="Import history table"> <column xsi:type="int" name="history_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="History record Id"/> + comment="History record ID"/> <column xsi:type="timestamp" name="started_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Started at"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index 909b526e4790c..2a9e1d388754f 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -11,6 +11,7 @@ <preference for="Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface" type="Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator" /> <preference for="Magento\ImportExport\Model\Report\ReportProcessorInterface" type="Magento\ImportExport\Model\Report\Csv" /> <preference for="Magento\ImportExport\Api\Data\ExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> + <preference for="Magento\ImportExport\Api\Data\ExtendedExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> <preference for="Magento\ImportExport\Api\ExportManagementInterface" type="Magento\ImportExport\Model\Export\ExportManagement" /> <type name="Magento\Framework\Module\Setup\Migration"> <arguments> diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index 5787d6f7d02b6..fae93c78baa09 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -29,9 +29,7 @@ Import,Import "File to Import","File to Import" "Select File to Import","Select File to Import" "Images File Directory","Images File Directory" -"For Type ""Local Server"" use relative path to Magento installation, - e.g. var/export, var/import, var/export/some/dir","For Type ""Local Server"" use relative path to Magento installation, - e.g. var/export, var/import, var/export/some/dir" +"For Type ""Local Server"" use relative path to <Magento root directory>/var/import/images, e.g. <i>product_images</i>, <i>import_images/batch1</i>.<br><br>For example, in case <i>product_images</i>, files should be placed into <i><Magento root directory>/var/import/images/product_images</i> folder.","For Type ""Local Server"" use relative path to <Magento root directory>/var/import/images, e.g. <i>product_images</i>, <i>import_images/batch1</i>.<br><br>For example, in case <i>product_images</i>, files should be placed into <i><Magento root directory>/var/import/images/product_images</i> folder." "Download Sample File","Download Sample File" "Please correct the data sent value.","Please correct the data sent value." Import/Export,Import/Export @@ -123,5 +121,6 @@ Summary,Summary "New product data is added to existing product data entries in the database. All fields except SKU can be updated.","New product data is added to existing product data entries in the database. All fields except SKU can be updated." "All existing product data is replaced with the imported new data. <b>Exercise caution when replacing data. All existing product data will be completely cleared and all references in the system will be lost.</b>","All existing product data is replaced with the imported new data. <b>Exercise caution when replacing data. All existing product data will be completely cleared and all references in the system will be lost.</b>" "Any entities in the import data that match existing entities in the database are deleted from the database.","Any entities in the import data that match existing entities in the database are deleted from the database." +"Message is added to queue, wait to get your file soon. Make sure your cron job is running to export the file","Message is added to queue, wait to get your file soon. Make sure your cron job is running to export the file" "Invalid data","Invalid data" -"Invalid response","Invalid response" \ No newline at end of file +"Invalid response","Invalid response" diff --git a/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml b/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml index 2b160bc9f6f40..1922f58ed47a0 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml +++ b/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml @@ -34,6 +34,9 @@ </settings> </dataProvider> </dataSource> + <listingToolbar name="listing_top"> + <paging name="listing_paging"/> + </listingToolbar> <columns name="export_grid_columns"> <column name="file_name"> <settings> diff --git a/app/code/Magento/Indexer/Console/Command/IndexerStatusCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerStatusCommand.php index cefb070f60b74..26feb38392e5f 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerStatusCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerStatusCommand.php @@ -5,11 +5,11 @@ */ namespace Magento\Indexer\Console\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Magento\Framework\Indexer; use Magento\Framework\Mview; use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; /** * Command for displaying status of indexers. @@ -17,7 +17,7 @@ class IndexerStatusCommand extends AbstractIndexerManageCommand { /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -29,12 +29,14 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc + * @param InputInterface $input + * @param OutputInterface $output */ protected function execute(InputInterface $input, OutputInterface $output) { $table = new Table($output); - $table->setHeaders(['Title', 'Status', 'Update On', 'Schedule Status', 'Schedule Updated']); + $table->setHeaders(['ID', 'Title', 'Status', 'Update On', 'Schedule Status', 'Schedule Updated']); $rows = []; @@ -43,6 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $view = $indexer->getView(); $rowData = [ + 'ID' => $indexer->getId(), 'Title' => $indexer->getTitle(), 'Status' => $this->getStatus($indexer), 'Update On' => $indexer->isScheduled() ? 'Schedule' : 'Save', @@ -59,15 +62,20 @@ protected function execute(InputInterface $input, OutputInterface $output) $rows[] = $rowData; } - usort($rows, function ($comp1, $comp2) { - return strcmp($comp1['Title'], $comp2['Title']); - }); + usort( + $rows, + function (array $comp1, array $comp2) { + return strcmp($comp1['Title'], $comp2['Title']); + } + ); $table->addRows($rows); $table->render(); } /** + * Returns the current status of the indexer + * * @param Indexer\IndexerInterface $indexer * @return string */ @@ -89,6 +97,8 @@ private function getStatus(Indexer\IndexerInterface $indexer) } /** + * Returns the pending count of the view + * * @param Mview\ViewInterface $view * @return string */ diff --git a/app/code/Magento/Indexer/Model/Indexer/DependencyDecorator.php b/app/code/Magento/Indexer/Model/Indexer/DependencyDecorator.php index 829df74a1b0ed..fcfdce4f58620 100644 --- a/app/code/Magento/Indexer/Model/Indexer/DependencyDecorator.php +++ b/app/code/Magento/Indexer/Model/Indexer/DependencyDecorator.php @@ -55,11 +55,15 @@ public function __construct( */ public function __call(string $method, array $args) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction return call_user_func_array([$this->indexer, $method], $args); } /** + * Sleep magic. + * * @return array + * @SuppressWarnings(PHPMD.SerializationAware) */ public function __sleep() { @@ -218,9 +222,16 @@ public function isWorking(): bool public function invalidate() { $this->indexer->invalidate(); - $dependentIndexerIds = $this->dependencyInfoProvider->getIndexerIdsToRunAfter($this->indexer->getId()); - foreach ($dependentIndexerIds as $indexerId) { - $this->indexerRegistry->get($indexerId)->invalidate(); + $currentIndexerId = $this->indexer->getId(); + $idsToRunBefore = $this->dependencyInfoProvider->getIndexerIdsToRunBefore($currentIndexerId); + $idsToRunAfter = $this->dependencyInfoProvider->getIndexerIdsToRunAfter($currentIndexerId); + + $indexersToInvalidate = array_unique(array_merge($idsToRunBefore, $idsToRunAfter)); + foreach ($indexersToInvalidate as $indexerId) { + $indexer = $this->indexerRegistry->get($indexerId); + if (!$indexer->isInvalid()) { + $indexer->invalidate(); + } } } diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml new file mode 100644 index 0000000000000..d474094dcd54b --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.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="AdminReindexAndFlushCache"> + <annotations> + <description>Run reindex and flush cache.</description> + </annotations> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php index 4877ceaaec85b..bdfeff8a89eb9 100644 --- a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Indexer\Test\Unit\Console\Command; use Magento\Framework\Console\Cli; @@ -85,23 +86,31 @@ public function testGetOptions() $this->stateMock->expects($this->never())->method('setAreaCode'); $this->command = new IndexerReindexCommand($this->objectManagerFactory); $optionsList = $this->command->getInputList(); - $this->assertSame(1, sizeof($optionsList)); + $this->assertSame(1, count($optionsList)); $this->assertSame('index', $optionsList[0]->getName()); } public function testExecuteAll() { - $this->configMock->expects($this->once())->method('getIndexer')->will($this->returnValue([ - 'title' => 'Title_indexerOne', - 'shared_index' => null - ])); + $this->configMock->expects($this->once()) + ->method('getIndexer') + ->will( + $this->returnValue( + [ + 'title' => 'Title_indexerOne', + 'shared_index' => null + ] + ) + ); $this->configureAdminArea(); - $this->initIndexerCollectionByItems([ - $this->getIndexerMock( - ['reindexAll', 'getStatus'], - ['indexer_id' => 'id_indexerOne', 'title' => 'Title_indexerOne'] - ) - ]); + $this->initIndexerCollectionByItems( + [ + $this->getIndexerMock( + ['reindexAll', 'getStatus'], + ['indexer_id' => 'id_indexerOne', 'title' => 'Title_indexerOne'] + ) + ] + ); $this->indexerFactory->expects($this->never())->method('create'); $this->command = new IndexerReindexCommand($this->objectManagerFactory); $commandTester = new CommandTester($this->command); @@ -118,6 +127,7 @@ public function testExecuteAll() * @param array $reindexAllCallMatchers * @param array $executedIndexers * @param array $executedSharedIndexers + * * @dataProvider executeWithIndexDataProvider */ public function testExecuteWithIndex( @@ -210,6 +220,7 @@ private function addAllIndexersToConfigMock(array $indexers) /** * @param array|null $methods * @param array $data + * * @return \PHPUnit_Framework_MockObject_MockObject|StateInterface */ private function getStateMock(array $methods = null, array $data = []) @@ -329,7 +340,7 @@ public function executeWithIndexDataProvider() 'indexer_5' => $this->once(), ], 'executed_indexers' => ['indexer_3', 'indexer_1', 'indexer_5'], - 'executed_shared_indexers' => [['indexer_2'],['indexer_3']], + 'executed_shared_indexers' => [['indexer_2'], ['indexer_3']], ], 'With dependencies and multiple indexers in request' => [ 'inputIndexers' => [ @@ -445,13 +456,16 @@ public function testExecuteWithExceptionInGetIndexers() "The following requested index types are not supported: '" . join("', '", $inputIndexers) . "'." . PHP_EOL . 'Supported types: ' - . join(", ", array_map( - function ($item) { - /** @var IndexerInterface $item */ - $item->getId(); - }, - $this->indexerCollectionMock->getItems() - )) + . join( + ", ", + array_map( + function ($item) { + /** @var IndexerInterface $item */ + $item->getId(); + }, + $this->indexerCollectionMock->getItems() + ) + ) ); $this->command = new IndexerReindexCommand($this->objectManagerFactory); $commandTester = new CommandTester($this->command); diff --git a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerSetModeCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerSetModeCommandTest.php index ad3ae88816db4..da47753970169 100644 --- a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerSetModeCommandTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerSetModeCommandTest.php @@ -26,7 +26,7 @@ public function testGetOptions() $this->stateMock->expects($this->never())->method('setAreaCode')->with(FrontNameResolver::AREA_CODE); $this->command = new IndexerSetModeCommand($this->objectManagerFactory); $optionsList = $this->command->getInputList(); - $this->assertSame(2, sizeof($optionsList)); + $this->assertSame(2, count($optionsList)); $this->assertSame('mode', $optionsList[0]->getName()); $this->assertSame('index', $optionsList[1]->getName()); } diff --git a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerShowModeCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerShowModeCommandTest.php index fe6020cb07167..ef8fb58c1ef41 100644 --- a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerShowModeCommandTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerShowModeCommandTest.php @@ -23,7 +23,7 @@ public function testGetOptions() $this->stateMock->expects($this->never())->method('setAreaCode')->with(FrontNameResolver::AREA_CODE); $this->command = new IndexerShowModeCommand($this->objectManagerFactory); $optionsList = $this->command->getInputList(); - $this->assertSame(1, sizeof($optionsList)); + $this->assertSame(1, count($optionsList)); $this->assertSame('index', $optionsList[0]->getName()); } diff --git a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusCommandTest.php index 8498bd183af21..963a0b21c1f96 100644 --- a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusCommandTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerStatusCommandTest.php @@ -96,7 +96,8 @@ public function testExecuteAll(array $indexers) $linesOutput = array_filter(explode(PHP_EOL, $commandTester->getDisplay())); - $spacer = '+----------------+------------------+-----------+-------------------------+---------------------+'; + $spacer = '+-----------+----------------+------------------+-----------+-------------------------+' + . '---------------------+'; $this->assertCount(8, $linesOutput, 'There should be 8 lines output. 3 Spacers, 1 header, 4 content.'); $this->assertEquals($linesOutput[0], $spacer, "Lines 0, 2, 7 should be spacer lines"); @@ -104,39 +105,44 @@ public function testExecuteAll(array $indexers) $this->assertEquals($linesOutput[7], $spacer, "Lines 0, 2, 7 should be spacer lines"); $headerValues = array_values(array_filter(explode('|', $linesOutput[1]))); - $this->assertEquals('Title', trim($headerValues[0])); - $this->assertEquals('Status', trim($headerValues[1])); - $this->assertEquals('Update On', trim($headerValues[2])); - $this->assertEquals('Schedule Status', trim($headerValues[3])); - $this->assertEquals('Schedule Updated', trim($headerValues[4])); + $this->assertEquals('ID', trim($headerValues[0])); + $this->assertEquals('Title', trim($headerValues[1])); + $this->assertEquals('Status', trim($headerValues[2])); + $this->assertEquals('Update On', trim($headerValues[3])); + $this->assertEquals('Schedule Status', trim($headerValues[4])); + $this->assertEquals('Schedule Updated', trim($headerValues[5])); $indexer1 = array_values(array_filter(explode('|', $linesOutput[3]))); - $this->assertEquals('Title_indexer1', trim($indexer1[0])); - $this->assertEquals('Ready', trim($indexer1[1])); - $this->assertEquals('Schedule', trim($indexer1[2])); - $this->assertEquals('idle (10 in backlog)', trim($indexer1[3])); - $this->assertEquals('2017-01-01 11:11:11', trim($indexer1[4])); + $this->assertEquals('indexer_1', trim($indexer1[0])); + $this->assertEquals('Title_indexer1', trim($indexer1[1])); + $this->assertEquals('Ready', trim($indexer1[2])); + $this->assertEquals('Schedule', trim($indexer1[3])); + $this->assertEquals('idle (10 in backlog)', trim($indexer1[4])); + $this->assertEquals('2017-01-01 11:11:11', trim($indexer1[5])); $indexer2 = array_values(array_filter(explode('|', $linesOutput[4]))); - $this->assertEquals('Title_indexer2', trim($indexer2[0])); - $this->assertEquals('Reindex required', trim($indexer2[1])); - $this->assertEquals('Save', trim($indexer2[2])); - $this->assertEquals('', trim($indexer2[3])); + $this->assertEquals('indexer_2', trim($indexer2[0])); + $this->assertEquals('Title_indexer2', trim($indexer2[1])); + $this->assertEquals('Reindex required', trim($indexer2[2])); + $this->assertEquals('Save', trim($indexer2[3])); $this->assertEquals('', trim($indexer2[4])); + $this->assertEquals('', trim($indexer2[5])); $indexer3 = array_values(array_filter(explode('|', $linesOutput[5]))); - $this->assertEquals('Title_indexer3', trim($indexer3[0])); - $this->assertEquals('Processing', trim($indexer3[1])); - $this->assertEquals('Schedule', trim($indexer3[2])); - $this->assertEquals('idle (100 in backlog)', trim($indexer3[3])); - $this->assertEquals('2017-01-01 11:11:11', trim($indexer3[4])); + $this->assertEquals('indexer_3', trim($indexer3[0])); + $this->assertEquals('Title_indexer3', trim($indexer3[1])); + $this->assertEquals('Processing', trim($indexer3[2])); + $this->assertEquals('Schedule', trim($indexer3[3])); + $this->assertEquals('idle (100 in backlog)', trim($indexer3[4])); + $this->assertEquals('2017-01-01 11:11:11', trim($indexer3[5])); $indexer4 = array_values(array_filter(explode('|', $linesOutput[6]))); - $this->assertEquals('Title_indexer4', trim($indexer4[0])); - $this->assertEquals('unknown', trim($indexer4[1])); - $this->assertEquals('Schedule', trim($indexer4[2])); - $this->assertEquals('running (20 in backlog)', trim($indexer4[3])); - $this->assertEquals('2017-01-01 11:11:11', trim($indexer4[4])); + $this->assertEquals('indexer_4', trim($indexer4[0])); + $this->assertEquals('Title_indexer4', trim($indexer4[1])); + $this->assertEquals('unknown', trim($indexer4[2])); + $this->assertEquals('Schedule', trim($indexer4[3])); + $this->assertEquals('running (20 in backlog)', trim($indexer4[4])); + $this->assertEquals('2017-01-01 11:11:11', trim($indexer4[5])); } /** diff --git a/app/code/Magento/Indexer/etc/db_schema.xml b/app/code/Magento/Indexer/etc/db_schema.xml index d7cb006a2cf45..c9c8e665b3755 100644 --- a/app/code/Magento/Indexer/etc/db_schema.xml +++ b/app/code/Magento/Indexer/etc/db_schema.xml @@ -9,8 +9,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="indexer_state" resource="default" engine="innodb" comment="Indexer State"> <column xsi:type="int" name="state_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Indexer State Id"/> - <column xsi:type="varchar" name="indexer_id" nullable="true" length="255" comment="Indexer Id"/> + comment="Indexer State ID"/> + <column xsi:type="varchar" name="indexer_id" nullable="true" length="255" comment="Indexer ID"/> <column xsi:type="varchar" name="status" nullable="true" length="16" default="invalid" comment="Indexer Status"/> <column xsi:type="datetime" name="updated" on_update="false" nullable="true" comment="Indexer Status"/> @@ -24,13 +24,13 @@ </table> <table name="mview_state" resource="default" engine="innodb" comment="View State"> <column xsi:type="int" name="state_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="View State Id"/> - <column xsi:type="varchar" name="view_id" nullable="true" length="255" comment="View Id"/> + comment="View State ID"/> + <column xsi:type="varchar" name="view_id" nullable="true" length="255" comment="View ID"/> <column xsi:type="varchar" name="mode" nullable="true" length="16" default="disabled" comment="View Mode"/> <column xsi:type="varchar" name="status" nullable="true" length="16" default="idle" comment="View Status"/> <column xsi:type="datetime" name="updated" on_update="false" nullable="true" comment="View updated time"/> <column xsi:type="int" name="version_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="View Version Id"/> + comment="View Version ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="state_id"/> </constraint> diff --git a/app/code/Magento/InstantPurchase/Test/Unit/Block/ButtonTest.php b/app/code/Magento/InstantPurchase/Test/Unit/Block/ButtonTest.php new file mode 100644 index 0000000000000..37d2729cd2a28 --- /dev/null +++ b/app/code/Magento/InstantPurchase/Test/Unit/Block/ButtonTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\InstantPurchase\Test\Unit\Block; + +use Magento\InstantPurchase\Block\Button; +use Magento\InstantPurchase\Model\Config; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Test class for button block + * + * Class \Magento\InstantPurchase\Test\Unit\Block\ButtonTest + */ +class ButtonTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Button | \PHPUnit_Framework_MockObject_MockObject + */ + private $block; + + /** + * @var Config | \PHPUnit_Framework_MockObject_MockObject + */ + private $config; + + /** + * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var StoreInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $store; + + /** + * @var Context | \PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * Setup environment for testing + */ + protected function setUp() + { + $this->context = $this->createMock(Context::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->store = $this->createMock(StoreInterface::class); + + $this->storeManager->expects($this->any())->method('getStore') + ->willReturn($this->store); + + $this->config = $this->createMock(Config::class); + + $this->context->expects($this->any())->method('getStoreManager') + ->willReturn($this->storeManager); + + $this->block = $this->getMockBuilder(Button::class) + ->setConstructorArgs( + [ + 'context' => $this->context, + 'instantPurchaseConfig' => $this->config + ] + ) + ->setMethods(['getUrl']) + ->getMock(); + } + + /** + * Test isEnabled() function + * + * @param $currentStoreId + * @param $isModuleEnabled + * @param $expected + * @dataProvider isEnabledDataProvider + */ + public function testIsEnabled($currentStoreId, $isModuleEnabled, $expected) + { + $this->store->expects($this->any())->method('getId') + ->willReturn($currentStoreId); + + $this->config->expects($this->any())->method('isModuleEnabled') + ->willReturn($isModuleEnabled); + + $this->assertEquals($expected, $this->block->isEnabled()); + } + + /** + * Data Provider for test isEnabled() + * + * @return array + */ + public function isEnabledDataProvider() + { + return [ + 'Store With ID = 1 and enable module' => [ + 1, + true, + true + ], + 'Store With ID = 1 and disable module' => [ + 1, + false, + false + ] + ]; + } + + /** + * Test getJsLayout() function + */ + public function testGetJsLayout() + { + $currentStoreId = 1; + $buttonText = 'Instant Purchased'; + $url = 'https://magento2.com/instantpurchase/button/placeOrder'; + $expected = '{"components":{"instant-purchase":{"config":{"buttonText":"Instant Purchased",' . + '"purchaseUrl":"https:\/\/magento2.com\/instantpurchase\/button\/placeOrder"}}}}'; + + $this->store->expects($this->any())->method('getId') + ->willReturn($currentStoreId); + $this->config->expects($this->any())->method('getButtonText') + ->willReturn($buttonText); + $this->block->expects($this->any())->method('getUrl') + ->with('instantpurchase/button/placeOrder', ['_secure' => true]) + ->willReturn($url); + + $this->assertEquals($expected, $this->block->getJsLayout()); + } +} diff --git a/app/code/Magento/InstantPurchase/Test/Unit/CustomerData/InstantPurchaseTest.php b/app/code/Magento/InstantPurchase/Test/Unit/CustomerData/InstantPurchaseTest.php new file mode 100644 index 0000000000000..c608338fd10c5 --- /dev/null +++ b/app/code/Magento/InstantPurchase/Test/Unit/CustomerData/InstantPurchaseTest.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\InstantPurchase\Test\Unit\CustomerData; + +use Magento\InstantPurchase\CustomerData\InstantPurchase as CustomerData; +use Magento\Customer\Model\Session; +use Magento\InstantPurchase\Model\InstantPurchaseInterface as InstantPurchaseModel; +use Magento\InstantPurchase\Model\Ui\CustomerAddressesFormatter; +use Magento\InstantPurchase\Model\Ui\PaymentTokenFormatter; +use Magento\InstantPurchase\Model\Ui\ShippingMethodFormatter; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\InstantPurchase\Model\InstantPurchaseOption; +use Magento\Customer\Model\Customer; + +/** + * Test class for InstantPurchase Customer Data + * + * Class \Magento\InstantPurchase\Test\Unit\CustomerData\InstantPurchaseTest + */ +class InstantPurchaseTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var objectManagerHelper + */ + private $objectManager; + + /** + * @var CustomerData | \PHPUnit_Framework_MockObject_MockObject + */ + private $customerData; + + /** + * @var Session | \PHPUnit_Framework_MockObject_MockObject + */ + private $customerSession; + + /** + * @var StoreManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var InstantPurchaseModel | \PHPUnit_Framework_MockObject_MockObject + */ + private $instantPurchase; + + /** + * @var PaymentTokenFormatter | \PHPUnit_Framework_MockObject_MockObject + */ + private $paymentTokenFormatter; + + /** + * @var CustomerAddressesFormatter | \PHPUnit_Framework_MockObject_MockObject + */ + private $customerAddressesFormatter; + + /** + * @var ShippingMethodFormatter | \PHPUnit_Framework_MockObject_MockObject + */ + private $shippingMethodFormatter; + + /** + * @var Store | \PHPUnit_Framework_MockObject_MockObject + */ + private $store; + + /** + * @var Customer | \PHPUnit_Framework_MockObject_MockObject + */ + private $customer; + + /** + * @var InstantPurchaseOption | \PHPUnit_Framework_MockObject_MockObject + */ + private $instantPurchaseOption; + + /** + * Setup environment for testing + */ + protected function setUp() + { + $this->customerSession = $this->createMock(Session::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->instantPurchase = $this->createMock(InstantPurchaseModel::class); + $this->paymentTokenFormatter = $this->createMock(PaymentTokenFormatter::class); + $this->customerAddressesFormatter = $this->createMock(CustomerAddressesFormatter::class); + $this->shippingMethodFormatter = $this->createMock(ShippingMethodFormatter::class); + $this->store = $this->createMock(Store::class); + $this->customer = $this->createMock(Customer::class); + $this->instantPurchaseOption = $this->createMock(InstantPurchaseOption::class); + + $this->objectManager = new ObjectManagerHelper($this); + $this->customerData = $this->objectManager->getObject( + CustomerData::class, + [ + 'customerSession' => $this->customerSession, + 'storeManager' => $this->storeManager, + 'instantPurchase' => $this->instantPurchase, + 'paymentTokenFormatter' => $this->paymentTokenFormatter, + 'customerAddressesFormatter' => $this->customerAddressesFormatter, + 'shippingMethodFormatter' => $this->shippingMethodFormatter + ] + ); + } + + /** + * Test getSectionData() + * + * @param $isLogin + * @param $isAvailable + * @param $expected + * @dataProvider getSectionDataProvider + */ + public function testGetSectionData($isLogin, $isAvailable, $expected) + { + $this->customerSession->expects($this->any())->method('isLoggedIn')->willReturn($isLogin); + + $this->storeManager->expects($this->any())->method('getStore')->willReturn($this->store); + + $this->customerSession->expects($this->any())->method('getCustomer') + ->willReturn($this->customer); + + $this->instantPurchase->expects($this->any())->method('getOption') + ->with($this->store, $this->customer) + ->willReturn($this->instantPurchaseOption); + + $this->instantPurchaseOption->expects($this->any())->method('isAvailable') + ->willReturn($isAvailable); + + $this->assertEquals($expected, $this->customerData->getSectionData()); + } + + /** + * Data Provider for test getSectionData() + * + * @return array + */ + public function getSectionDataProvider() + { + return [ + 'No Login and available instant purchase' => [ + false, + true, + ['available' => false] + ], + + 'Login and no available instant purchase option' => [ + true, + false, + ['available' => false] + ], + + 'Login and available instant purchase option' => [ + true, + true, + [ + 'available' => true, + 'paymentToken' => [ + 'publicHash' => '', + 'summary' => '' + ], + 'shippingAddress' => [ + 'id' => null, + 'summary' => '' + ], + 'billingAddress' => [ + 'id' => null, + 'summary' => '' + ], + 'shippingMethod' => [ + 'carrier' => null, + 'method' => null, + 'summary' => '' + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php b/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php index 4042c2ebde87d..89cad471933e6 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Integration/Edit/Tab/Info.php @@ -179,7 +179,7 @@ protected function _addGeneralFieldset($form, $integrationData) 'label' => __('Your Password'), 'id' => self::DATA_CONSUMER_PASSWORD, 'title' => __('Your Password'), - 'class' => 'input-text validate-current-password required-entry', + 'class' => 'validate-current-password required-entry', 'required' => true ] ); diff --git a/app/code/Magento/Integration/Helper/Oauth/Data.php b/app/code/Magento/Integration/Helper/Oauth/Data.php index de074055efa2a..107583a9e70a8 100644 --- a/app/code/Magento/Integration/Helper/Oauth/Data.php +++ b/app/code/Magento/Integration/Helper/Oauth/Data.php @@ -116,22 +116,22 @@ public function getConsumerPostTimeout() /** * Get customer token lifetime from config. * - * @return int hours + * @return float hours */ public function getCustomerTokenLifetime() { - $hours = (int)$this->_scopeConfig->getValue('oauth/access_token_lifetime/customer'); - return $hours > 0 ? $hours : 0; + $hours = $this->_scopeConfig->getValue('oauth/access_token_lifetime/customer'); + return is_numeric($hours) && $hours > 0 ? $hours : 0; } /** * Get customer token lifetime from config. * - * @return int hours + * @return float hours */ public function getAdminTokenLifetime() { - $hours = (int)$this->_scopeConfig->getValue('oauth/access_token_lifetime/admin'); - return $hours > 0 ? $hours : 0; + $hours = $this->_scopeConfig->getValue('oauth/access_token_lifetime/admin'); + return is_numeric($hours) && $hours > 0 ? $hours : 0; } } diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminCreatesNewIntegrationActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminCreatesNewIntegrationActionGroup.xml new file mode 100644 index 0000000000000..c039a70293269 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminCreatesNewIntegrationActionGroup.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"> + + <!--Fill Required Fields --> + <actionGroup name="AdminCreatesNewIntegrationActionGroup"> + <arguments> + <argument name="name" type="string"/> + <argument name="password" type="string"/> + </arguments> + <fillField stepKey="fillNameField" selector="{{AdminNewIntegrationSection.name}}" userInput="{{name}}"/> + <fillField stepKey="fillAdminPasswordField" selector="{{AdminNewIntegrationSection.password}}" userInput="{{password}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminDeleteIntegrationEntityActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminDeleteIntegrationEntityActionGroup.xml new file mode 100644 index 0000000000000..4a73f6ce4b8df --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminDeleteIntegrationEntityActionGroup.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="AdminDeleteIntegrationEntityActionGroup"> + <click stepKey="clickRemoveButon" selector="{{AdminIntegrationsGridSection.remove}}"/> + <waitForElementVisible selector="{{AdminIntegrationsGridSection.submitButton}}" stepKey="waitForConfirmButtonVisible"/> + <click stepKey="clickSubmitButton" selector="{{AdminIntegrationsGridSection.submitButton}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminNavigateToCreateIntegrationPageActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminNavigateToCreateIntegrationPageActionGroup.xml new file mode 100644 index 0000000000000..18deddb3170aa --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminNavigateToCreateIntegrationPageActionGroup.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"> + + <!--Click the "Add New Integration" Button --> + + <actionGroup name="AdminNavigateToCreateIntegrationPageActionGroup"> + <click stepKey="clickAddNewIntegrationButton" selector="{{AdminIntegrationsGridSection.add}}"/> + <waitForPageLoad stepKey="waitForNewNIntegrationPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminSearchIntegrationInGridActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminSearchIntegrationInGridActionGroup.xml new file mode 100644 index 0000000000000..dcd60a0479db4 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminSearchIntegrationInGridActionGroup.xml @@ -0,0 +1,23 @@ +<?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="AdminSearchIntegrationInGridActionGroup"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <!--Reset Search Filters --> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <!--Fill Integration Name Field --> + <fillField selector="{{AdminIntegrationsGridSection.name}}" userInput="{{name}}" stepKey="filterByName"/> + <!--Click "Search" Button --> + <click selector="{{AdminIntegrationsGridSection.search}}" stepKey="doFilter"/> + <waitForPageLoad stepKey="waitForSitemapPageLoadedAfterFiltering"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminSubmitNewIntegrationFormActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminSubmitNewIntegrationFormActionGroup.xml new file mode 100644 index 0000000000000..f1dcd5da77c85 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminSubmitNewIntegrationFormActionGroup.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="AdminSubmitNewIntegrationFormActionGroup"> + <!--Click the "Save" Button --> + <click stepKey="clickSaveButton" selector="{{AdminNewIntegrationSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminMessageCreateIntegrationEntityActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminMessageCreateIntegrationEntityActionGroup.xml new file mode 100644 index 0000000000000..f233c1d9a7f74 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminMessageCreateIntegrationEntityActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AssertAdminMessageCreateIntegrationEntityActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="The integration '{{name}}' has been saved."/> + <argument name="messageType" type="string" defaultValue="success"/> + </arguments> + <waitForElementVisible selector="{{AdminIntegrationsGridSection.messageByType(messageType)}}" stepKey="waitForMessage"/> + <see userInput="{{message}}" selector="{{AdminIntegrationsGridSection.messageByType(messageType)}}" stepKey="verifyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertDeletedIntegrationIsNotInGridActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertDeletedIntegrationIsNotInGridActionGroup.xml new file mode 100644 index 0000000000000..a4438b1eb70a7 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertDeletedIntegrationIsNotInGridActionGroup.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="AssertDeletedIntegrationIsNotInGridActionGroup"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <dontSee userInput="{{name}}" selector="{{AdminIntegrationsGridSection.rowByIndex('1')}}" stepKey="donSeeIntegration"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/Section/AdminIntegrationsGridSection.xml b/app/code/Magento/Integration/Test/Mftf/Section/AdminIntegrationsGridSection.xml new file mode 100644 index 0000000000000..ae601542f3b37 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Section/AdminIntegrationsGridSection.xml @@ -0,0 +1,21 @@ +<?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="AdminIntegrationsGridSection"> + <element name="add" type="button" selector=".page-actions .add"/> + <element name="messageByType" type="block" selector="#messages .message-{{messageType}}" parameterized="true"/> + <element name="name" type="input" selector=".data-grid-filters #integrationGrid_filter_name"/> + <element name="search" type="input" selector=".admin__filter-actions button[title=Search]"/> + <element name="remove" type="button" selector=".data-grid .delete"/> + <element name="submitButton" type="button" selector=".action-primary.action-accept" timeout="30"/> + <element name="rowByIndex" type="text" selector="tr[data-role='row']:nth-of-type({{var1}})" parameterized="true" timeout="30"/> + <element name="edit" type="button" selector=".data-grid .edit"/> + </section> +</sections> diff --git a/app/code/Magento/Integration/Test/Mftf/Section/AdminNewIntegrationSection.xml b/app/code/Magento/Integration/Test/Mftf/Section/AdminNewIntegrationSection.xml new file mode 100644 index 0000000000000..3e7214784c2b5 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Section/AdminNewIntegrationSection.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="AdminNewIntegrationSection"> + <element name="name" type="input" selector="#integration_properties_name"/> + <element name="password" type="input" selector="#integration_properties_current_password"/> + <element name="saveButton" type="button" selector=".page-actions #save-split-button-button"/> + <element name="endpoint" type="input" selector="#integration_properties_endpoint"/> + <element name="linkUrl" type="input" selector="#integration_properties_identity_link_url"/> + <element name="save" type="button" selector=".page-actions-buttons .save"/> + </section> +</sections> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml new file mode 100644 index 0000000000000..6f46bbf99d218 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml @@ -0,0 +1,63 @@ +<?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="AdminDeleteIntegrationEntityTest"> + <annotations> + <features value="Integration"/> + <stories value="System Integration"/> + <title value="Admin system integration"/> + <description value="Admin Deletes Created Integration"/> + <group value="integration"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login As Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <!-- Navigate To Integrations Page --> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToIntegrationsPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemExtensionsIntegrations.dataUiId}}"/> + </actionGroup> + <!-- Click the "Add New Integration" button --> + <actionGroup ref="AdminNavigateToCreateIntegrationPageActionGroup" stepKey="clickAddNewIntegrationButton"/> + <!-- Create New Integration --> + <actionGroup ref="AdminCreatesNewIntegrationActionGroup" stepKey="createIntegration"> + <argument name="name" value="Integration1"/> + <argument name="password" value="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + </actionGroup> + <!-- Submit The Form --> + <actionGroup ref="AdminSubmitNewIntegrationFormActionGroup" stepKey="submitTheForm"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- TEST BODY --> + <!-- Find Created Integration In Grid --> + <actionGroup ref="AdminSearchIntegrationInGridActionGroup" stepKey="findCreatedIntegration"> + <argument name="name" value="Integration1"/> + </actionGroup> + <!-- Delete Created Integration Entity --> + <actionGroup ref="AdminDeleteIntegrationEntityActionGroup" stepKey="deleteIntegration"/> + <!-- Assert Success Message --> + <actionGroup ref="AssertAdminMessageCreateIntegrationEntityActionGroup" stepKey="seeSuccessMessage"> + <argument name="message" value="The integration 'Integration1' has been deleted."/> + <argument value="success" name="messageType"/> + </actionGroup> + <!-- Assert Deleted Integration Is Not In Grid --> + <actionGroup ref="AdminSearchIntegrationInGridActionGroup" stepKey="findDeletedIntegration"> + <argument name="name" value="Integration1"/> + </actionGroup> + <actionGroup ref="AssertDeletedIntegrationIsNotInGridActionGroup" stepKey="dontSeeIntegration"> + <argument name="name" value="Integration1"/> + </actionGroup> + <!-- END TEST BODY --> + </test> +</tests> diff --git a/app/code/Magento/Integration/etc/db_schema.xml b/app/code/Magento/Integration/etc/db_schema.xml index cbf43d79b2cf6..de0cec2e4e20d 100644 --- a/app/code/Magento/Integration/etc/db_schema.xml +++ b/app/code/Magento/Integration/etc/db_schema.xml @@ -129,7 +129,7 @@ <table name="oauth_token_request_log" resource="default" engine="innodb" comment="Log of token request authentication failures."> <column xsi:type="int" name="log_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Log Id"/> + comment="Log ID"/> <column xsi:type="varchar" name="user_name" nullable="false" length="255" comment="Customer email or admin login"/> <column xsi:type="smallint" name="user_type" padding="5" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml b/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml index 7d6c27954d836..1737f66ce4a1b 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml @@ -38,13 +38,13 @@ <label class="label"><span><?= $block->escapeHtml(__('Resources')) ?></span></label> <div class="control"> - <div class="tree x-tree" data-role="resource-tree" data-mage-init='<?= /* @noEscape */ - $block->getJsonSerializer()->serialize([ + <div class="tree x-tree" data-role="resource-tree" data-mage-init='<?= + $block->escapeHtmlAttr($block->getJsonSerializer()->serialize([ 'rolesTree' => [ "treeInitData" => $block->getTree(), "treeInitSelectedData" => $block->getSelectedResources(), ], - ]); ?>'> + ])); ?>'> </div> </div> </div> diff --git a/app/code/Magento/LayeredNavigation/Block/Navigation.php b/app/code/Magento/LayeredNavigation/Block/Navigation.php index 25c6edd1d61aa..e394fe7f6cf5b 100644 --- a/app/code/Magento/LayeredNavigation/Block/Navigation.php +++ b/app/code/Magento/LayeredNavigation/Block/Navigation.php @@ -121,7 +121,8 @@ public function getFilters() */ public function canShowBlock() { - return $this->visibilityFlag->isEnabled($this->getLayer(), $this->getFilters()); + return $this->getLayer()->getCurrentCategory()->getDisplayMode() !== \Magento\Catalog\Model\Category::DM_PAGE + && $this->visibilityFlag->isEnabled($this->getLayer(), $this->getFilters()); } /** diff --git a/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php b/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php index 0b9cb377d1d08..ce618f97883b0 100644 --- a/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php +++ b/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php @@ -8,7 +8,7 @@ namespace Magento\LayeredNavigation\Observer\Edit\Tab\Front; use Magento\Config\Model\Config\Source; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\Event\ObserverInterface; /** @@ -22,15 +22,15 @@ class ProductAttributeFormBuildFrontTabObserver implements ObserverInterface protected $optionList; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param Source\Yesno $optionList */ - public function __construct(ModuleManagerInterface $moduleManager, Source\Yesno $optionList) + public function __construct(Manager $moduleManager, Source\Yesno $optionList) { $this->optionList = $optionList; $this->moduleManager = $moduleManager; diff --git a/app/code/Magento/LayeredNavigation/Observer/Grid/ProductAttributeGridBuildObserver.php b/app/code/Magento/LayeredNavigation/Observer/Grid/ProductAttributeGridBuildObserver.php index b98230c1ebe3c..57a20cf17371d 100644 --- a/app/code/Magento/LayeredNavigation/Observer/Grid/ProductAttributeGridBuildObserver.php +++ b/app/code/Magento/LayeredNavigation/Observer/Grid/ProductAttributeGridBuildObserver.php @@ -7,7 +7,7 @@ */ namespace Magento\LayeredNavigation\Observer\Grid; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\Event\ObserverInterface; /** @@ -16,16 +16,16 @@ class ProductAttributeGridBuildObserver implements ObserverInterface { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** * Construct. * - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager */ - public function __construct(ModuleManagerInterface $moduleManager) + public function __construct(Manager $moduleManager) { $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml index 1e4137beacd88..b3e0c430b12e7 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="LayeredNavigationSection"> + <element name="filterOptionContent" type="text" selector="//div[contains(text(), '{{attribute}}')]//following-sibling::div//a[contains(text(), '{{option}}')]" parameterized="true"/> <element name="layeredNavigation" type="select" selector="#catalog_layered_navigation-head"/> <element name="layeredNavigationBlock" type="block" selector="#catalog_layered_navigation"/> <element name="CheckIfTabExpand" type="button" selector="#catalog_layered_navigation-head:not(.open)"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml new file mode 100644 index 0000000000000..7b78b5193ef7c --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -0,0 +1,35 @@ +<?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="AdminCheckResultsOfColorAndOtherFiltersTest"> + <!-- Open a category on storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" after="flushCache" stepKey="goToCategoryPage"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <!-- Choose First attribute filter --> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$$createConfigProductAttribute.default_frontend_label$$')}}" stepKey="waitForCartRuleButton"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$$createConfigProductAttribute.default_frontend_label$$')}}" stepKey="expandFirstAttribute"/> + <waitForPageLoad stepKey="waitForFilterLoad"/> + <click selector="{{LayeredNavigationSection.filterOptionContent('$$createConfigProductAttribute.default_frontend_label$$','option2')}}" stepKey="expandFirstAttributeOption"/> + <waitForPageLoad stepKey="waitForAttributeOption"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($$createFirstConfigurableProduct.name$$)}}" stepKey="seeFirstProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($$createSecondConfigurableProduct.name$$)}}" stepKey="seeSecondProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($$createThirdConfigurableProduct.name$$)}}" stepKey="seeSimpleProduct"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryPageAgain"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <!-- Choose Second attribute filter --> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$$createConfigProductAttribute2.default_frontend_label$$')}}" stepKey="expandSecondAttributeOption"/> + <waitForPageLoad stepKey="waitForFilterPageLoad"/> + <click selector="{{LayeredNavigationSection.filterOptionContent('$$createConfigProductAttribute2.default_frontend_label$$','option1')}}" stepKey="expandSecondAttribute"/> + <waitForPageLoad stepKey="waitForProductListLoad"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($$createFirstConfigurableProduct.name$$)}}" stepKey="seeFourthProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($$createSecondConfigurableProduct.name$$)}}" stepKey="seeFifthProduct"/> + </test> +</tests> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml index 721942f58f7cc..6d182d0b7a5e2 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6092"/> <group value="LayeredNavigation"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php b/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php index e37e58b14f027..34627fbb286ed 100644 --- a/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php +++ b/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php @@ -6,6 +6,8 @@ namespace Magento\LayeredNavigation\Test\Unit\Block; +use Magento\Catalog\Model\Category; + class NavigationTest extends \PHPUnit\Framework\TestCase { /** @@ -98,9 +100,64 @@ public function testCanShowBlock() ->method('isEnabled') ->with($this->catalogLayerMock, $filters) ->will($this->returnValue($enabled)); + + $category = $this->createMock(Category::class); + $this->catalogLayerMock->expects($this->atLeastOnce())->method('getCurrentCategory')->willReturn($category); + $category->expects($this->once())->method('getDisplayMode')->willReturn(Category::DM_PRODUCT); + $this->assertEquals($enabled, $this->model->canShowBlock()); } + /** + * Test canShowBlock() with different category display types. + * + * @param string $mode + * @param bool $result + * + * @dataProvider canShowBlockDataProvider + */ + public function testCanShowBlockWithDifferentDisplayModes(string $mode, bool $result) + { + $filters = ['To' => 'be', 'or' => 'not', 'to' => 'be']; + + $this->filterListMock->expects($this->atLeastOnce())->method('getFilters') + ->with($this->catalogLayerMock) + ->will($this->returnValue($filters)); + $this->assertEquals($filters, $this->model->getFilters()); + + $this->visibilityFlagMock + ->expects($this->any()) + ->method('isEnabled') + ->with($this->catalogLayerMock, $filters) + ->will($this->returnValue(true)); + + $category = $this->createMock(Category::class); + $this->catalogLayerMock->expects($this->atLeastOnce())->method('getCurrentCategory')->willReturn($category); + $category->expects($this->once())->method('getDisplayMode')->willReturn($mode); + $this->assertEquals($result, $this->model->canShowBlock()); + } + + /** + * @return array + */ + public function canShowBlockDataProvider() + { + return [ + [ + Category::DM_PRODUCT, + true, + ], + [ + Category::DM_PAGE, + false, + ], + [ + Category::DM_MIXED, + true, + ], + ]; + } + public function testGetClearUrl() { $this->filterListMock->expects($this->any())->method('getFilters')->will($this->returnValue([])); diff --git a/app/code/Magento/MediaGallery/LICENSE.txt b/app/code/Magento/MediaGallery/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallery/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGallery/LICENSE_AFL.txt b/app/code/Magento/MediaGallery/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallery/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGallery/Model/Asset.php b/app/code/Magento/MediaGallery/Model/Asset.php new file mode 100644 index 0000000000000..f6e1c00044a79 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Asset.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\Framework\Model\AbstractExtensibleModel; + +/** + * Media Gallery Asset + */ +class Asset extends AbstractExtensibleModel implements AssetInterface +{ + private const ID = 'id'; + private const PATH = 'path'; + private const TITLE = 'title'; + private const SOURCE = 'source'; + private const CONTENT_TYPE = 'content_type'; + private const WIDTH = 'width'; + private const HEIGHT = 'height'; + private const CREATED_AT = 'created_at'; + private const UPDATED_AT = 'updated_at'; + + /** + * @inheritdoc + */ + public function getId(): ?int + { + $id = $this->getData(self::ID); + + if (!$id) { + return null; + } + + return (int) $id; + } + + /** + * @inheritdoc + */ + public function getPath(): string + { + return (string) $this->getData(self::PATH); + } + + /** + * @inheritdoc + */ + public function getTitle(): ?string + { + return $this->getData(self::TITLE); + } + + /** + * @inheritdoc + */ + public function getSource(): ?string + { + return $this->getData(self::SOURCE); + } + + /** + * @inheritdoc + */ + public function getContentType(): string + { + return (string) $this->getData(self::CONTENT_TYPE); + } + + /** + * @inheritdoc + */ + public function getWidth(): int + { + return (int) $this->getData(self::WIDTH); + } + + /** + * @inheritdoc + */ + public function getHeight(): int + { + return (int) $this->getData(self::HEIGHT); + } + + /** + * @inheritdoc + */ + public function getCreatedAt(): string + { + return (string) $this->getData(self::CREATED_AT); + } + + /** + * @inheritdoc + */ + public function getUpdatedAt(): string + { + return (string) $this->getData(self::UPDATED_AT); + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): AssetExtensionInterface + { + return $this->_getExtensionAttributes(); + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(AssetExtensionInterface $extensionAttributes): void + { + $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php new file mode 100644 index 0000000000000..c05a08149bfca --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Asset\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Exception\CouldNotDeleteException; +use Magento\MediaGalleryApi\Model\Asset\Command\DeleteByPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Class DeleteByPath + */ +class DeleteByPath implements DeleteByPathInterface +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + private const MEDIA_GALLERY_ASSET_PATH = 'path'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * DeleteById constructor. + * + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Delete media asset by path + * + * @param string $mediaAssetPath + * + * @return void + * @throws CouldNotDeleteException + */ + public function execute(string $mediaAssetPath): void + { + try { + /** @var AdapterInterface $connection */ + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET); + $connection->delete($tableName, [self::MEDIA_GALLERY_ASSET_PATH . ' = ?' => $mediaAssetPath]); + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __( + 'Could not delete media asset with path %path: %error', + ['path' => $mediaAssetPath, 'error' => $exception->getMessage()] + ); + throw new CouldNotDeleteException($message, $exception); + } + } +} diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php new file mode 100644 index 0000000000000..6be11610ac197 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Asset\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Model\Asset\Command\GetByIdInterface; +use Psr\Log\LoggerInterface; + +/** + * Class GetById + */ +class GetById implements GetByIdInterface +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var AssetInterface + */ + private $assetFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * GetById constructor. + * + * @param ResourceConnection $resourceConnection + * @param AssetInterfaceFactory $assetFactory + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + AssetInterfaceFactory $assetFactory, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->assetFactory = $assetFactory; + $this->logger = $logger; + } + + /** + * Get media asset. + * + * @param int $mediaAssetId + * + * @return AssetInterface + * @throws NoSuchEntityException + * @throws IntegrationException + */ + public function execute(int $mediaAssetId): AssetInterface + { + try { + $mediaAssetTable = $this->resourceConnection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET); + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from(['amg' => $mediaAssetTable]) + ->where('amg.id = ?', $mediaAssetId); + $mediaAssetData = $connection->query($select)->fetch(); + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __( + 'En error occurred during get media asset data by id %id: %error', + ['id' => $mediaAssetId, 'error' => $exception->getMessage()] + ); + throw new IntegrationException($message, $exception); + } + + if (empty($mediaAssetData)) { + $message = __('There is no such media asset with id %id', ['id' => $mediaAssetId]); + throw new NoSuchEntityException($message); + } + + try { + return $this->assetFactory->create(['data' => $mediaAssetData]); + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __( + 'En error occurred during initialize media asset with id %id: %error', + ['id' => $mediaAssetId, 'error' => $exception->getMessage()] + ); + throw new IntegrationException($message, $exception); + } + } +} diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php new file mode 100644 index 0000000000000..db8482d3399ba --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Asset\Command; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Model\Asset\Command\GetByPathInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Psr\Log\LoggerInterface; + +/** + * Class GetListByIds + */ +class GetByPath implements GetByPathInterface +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + private const MEDIA_GALLERY_ASSET_PATH = 'path'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var AssetInterface + */ + private $mediaAssetFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * GetByPath constructor. + * + * @param ResourceConnection $resourceConnection + * @param AssetInterfaceFactory $mediaAssetFactory + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + AssetInterfaceFactory $mediaAssetFactory, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->mediaAssetFactory = $mediaAssetFactory; + $this->logger = $logger; + } + + /** + * Return media asset asset list + * + * @param string $mediaFilePath + * + * @return AssetInterface + * @throws IntegrationException + */ + public function execute(string $mediaFilePath): AssetInterface + { + try { + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from($this->resourceConnection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET)) + ->where(self::MEDIA_GALLERY_ASSET_PATH . ' = ?', $mediaFilePath); + $data = $connection->query($select)->fetch(); + + if (empty($data)) { + $message = __('There is no such media asset with path "%1"', $mediaFilePath); + throw new NoSuchEntityException($message); + } + + $mediaAssets = $this->mediaAssetFactory->create(['data' => $data]); + + return $mediaAssets; + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __('An error occurred during get media asset list: %1', $exception->getMessage()); + throw new IntegrationException($message, $exception); + } + } +} diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php b/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php new file mode 100644 index 0000000000000..7cb2f73169642 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Asset\Command; + +use Magento\MediaGalleryApi\Model\DataExtractorInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Model\Asset\Command\SaveInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\CouldNotSaveException; +use Psr\Log\LoggerInterface; + +/** + * Class Save + */ +class Save implements SaveInterface +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DataExtractorInterface + */ + private $extractor; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Save constructor. + * + * @param ResourceConnection $resourceConnection + * @param DataExtractorInterface $extractor + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + DataExtractorInterface $extractor, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->extractor = $extractor; + $this->logger = $logger; + } + + /** + * Save media assets + * + * @param AssetInterface $mediaAsset + * + * @return int + * @throws CouldNotSaveException + */ + public function execute(AssetInterface $mediaAsset): int + { + try { + /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET); + + $connection->insertOnDuplicate($tableName, $this->extractor->extract($mediaAsset, AssetInterface::class)); + return (int) $connection->lastInsertId($tableName); + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __('An error occurred during media asset save: %1', $exception->getMessage()); + throw new CouldNotSaveException($message, $exception); + } + } +} diff --git a/app/code/Magento/MediaGallery/Model/DataExtractor.php b/app/code/Magento/MediaGallery/Model/DataExtractor.php new file mode 100644 index 0000000000000..92cf237022c28 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/DataExtractor.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\MediaGalleryApi\Model\DataExtractorInterface; + +/** + * Extract data from an object using available getters + */ +class DataExtractor implements DataExtractorInterface +{ + /** + * Extract data from an object using available getters (does not process extension attributes) + * + * @param object $object + * @param string|null $interface + * + * @return array + * @throws \ReflectionException + */ + public function extract($object, string $interface = null): array + { + $data = []; + + $reflectionClass = new \ReflectionClass($interface ?? $object); + + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $methodName = $method->getName(); + if (strpos($methodName, 'get') !== 0 + || !empty($method->getParameters()) + || strpos($methodName, 'getExtensionAttributes') !== false + ) { + continue; + } + $value = $object->$methodName(); + if (!empty($value)) { + $key = strtolower(preg_replace("/([a-z])([A-Z])/", "$1_$2", substr($methodName, 3))); + $data[$key] = $value; + } + } + return $data; + } +} diff --git a/app/code/Magento/MediaGallery/Model/Keyword.php b/app/code/Magento/MediaGallery/Model/Keyword.php new file mode 100644 index 0000000000000..c5c60d3152846 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Keyword.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\Framework\Model\AbstractExtensibleModel; + +/** + * Asset's Keyword + */ +class Keyword extends AbstractExtensibleModel implements KeywordInterface +{ + private const ID = 'id'; + + private const KEYWORD = 'keyword'; + + /** + * @inheritdoc + */ + public function getId(): ?int + { + $id = $this->getData(self::ID); + + if (!$id) { + return null; + } + + return (int) $id; + } + + /** + * @inheritdoc + */ + public function getKeyword(): string + { + return (string)$this->getData(self::KEYWORD); + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): KeywordExtensionInterface + { + return $this->_getExtensionAttributes(); + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(KeywordExtensionInterface $extensionAttributes): void + { + $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php new file mode 100644 index 0000000000000..5b826a26e937d --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Keyword\Command; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Model\Keyword\Command\GetAssetKeywordsInterface; +use Magento\Framework\App\ResourceConnection; +use Psr\Log\LoggerInterface; + +/** + * ClassGetAssetKeywords + */ +class GetAssetKeywords implements GetAssetKeywordsInterface +{ + private const TABLE_KEYWORD = 'media_gallery_keyword'; + private const TABLE_ASSET_KEYWORD = 'media_gallery_asset_keyword'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var KeywordInterfaceFactory + */ + private $assetKeywordFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * GetAssetKeywords constructor. + * + * @param ResourceConnection $resourceConnection + * @param KeywordInterfaceFactory $assetKeywordFactory + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + KeywordInterfaceFactory $assetKeywordFactory, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->assetKeywordFactory = $assetKeywordFactory; + $this->logger = $logger; + } + + /** + * Get asset related keywords. + * + * @param int $assetId + * + * @return KeywordInterface[]|[] + * @throws IntegrationException + */ + public function execute(int $assetId): array + { + try { + $connection = $this->resourceConnection->getConnection(); + + $select = $connection->select() + ->from(['k' => $this->resourceConnection->getTableName(self::TABLE_KEYWORD)]) + ->join(['ak' => self::TABLE_ASSET_KEYWORD], 'k.id = ak.keyword_id') + ->where('ak.asset_id = ?', $assetId); + $data = $connection->query($select)->fetchAll(); + + $keywords = []; + foreach ($data as $keywordData) { + $keywords[] = $this->assetKeywordFactory->create(['data' => $keywordData]); + } + + return $keywords; + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __('An error occurred during get asset keywords: %1', $exception->getMessage()); + throw new IntegrationException($message, $exception); + } + } +} diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php new file mode 100644 index 0000000000000..b355a9a651cd4 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Keyword\Command; + +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Model\Keyword\Command\SaveAssetKeywordsInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\Exception\CouldNotSaveException; +use Psr\Log\LoggerInterface; + +/** + * Class SaveAssetKeywords + */ +class SaveAssetKeywords implements SaveAssetKeywordsInterface +{ + private const TABLE_KEYWORD = 'media_gallery_keyword'; + private const ID = 'id'; + private const KEYWORD = 'keyword'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var SaveAssetLinks + */ + private $saveAssetLinks; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * SaveAssetKeywords constructor. + * + * @param ResourceConnection $resourceConnection + * @param SaveAssetLinks $saveAssetLinks + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + SaveAssetLinks $saveAssetLinks, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->saveAssetLinks = $saveAssetLinks; + $this->logger = $logger; + } + + /** + * Save asset keywords. + * + * @param KeywordInterface[] $keywords + * @param int $assetId + * @throws CouldNotSaveException + */ + public function execute(array $keywords, int $assetId): void + { + try { + $data = []; + /** @var KeywordInterface $keyword */ + foreach ($keywords as $keyword) { + $data[] = $keyword->getKeyword(); + } + + if (!empty($data)) { + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->insertArray( + $this->resourceConnection->getTableName(self::TABLE_KEYWORD), + [self::KEYWORD], + $data, + AdapterInterface::INSERT_IGNORE + ); + + $this->saveAssetLinks->execute($assetId, $this->getKeywordIds($data)); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __('An error occurred during save asset keyword: %1', $exception->getMessage()); + throw new CouldNotSaveException($message, $exception); + } + } + + /** + * Select keywords by names + * + * @param string[] $keywords + * + * @return int[] + */ + private function getKeywordIds(array $keywords): array + { + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from(['k' => $this->resourceConnection->getTableName(self::TABLE_KEYWORD)]) + ->columns(self::ID) + ->where('k.' . self::KEYWORD . ' in (?)', $keywords); + + return $connection->fetchCol($select); + } +} diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetLinks.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetLinks.php new file mode 100644 index 0000000000000..4d3fd2bb5c30d --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetLinks.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\Keyword\Command; + +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Model\Keyword\Command\SaveAssetLinksInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\Exception\CouldNotSaveException; +use Psr\Log\LoggerInterface; + +/** + * Class SaveAssetLinks + */ +class SaveAssetLinks +{ + private const TABLE_ASSET_KEYWORD = 'media_gallery_asset_keyword'; + private const FIELD_ASSET_ID = 'asset_id'; + private const FIELD_KEYWORD_ID = 'keyword_id'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * SaveAssetLinks constructor. + * + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Save asset keywords links + * + * @param int $assetId + * @param KeywordInterface[] $keywordIds + * + * @throws CouldNotSaveException + */ + public function execute(int $assetId, array $keywordIds): void + { + try { + $values = []; + foreach ($keywordIds as $keywordId) { + $values[] = [$assetId, $keywordId]; + } + + if (!empty($values)) { + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->insertArray( + $this->resourceConnection->getTableName(self::TABLE_ASSET_KEYWORD), + [self::FIELD_ASSET_ID, self::FIELD_KEYWORD_ID], + $values, + AdapterInterface::INSERT_IGNORE + ); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + $message = __('An error occurred during save asset keyword links: %1', $exception->getMessage()); + throw new CouldNotSaveException($message, $exception); + } + } +} diff --git a/app/code/Magento/MediaGallery/Plugin/Product/Gallery/Processor.php b/app/code/Magento/MediaGallery/Plugin/Product/Gallery/Processor.php new file mode 100644 index 0000000000000..3fbe4e3a91a2b --- /dev/null +++ b/app/code/Magento/MediaGallery/Plugin/Product/Gallery/Processor.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallery\Plugin\Product\Gallery; + +use Magento\MediaGalleryApi\Model\Asset\Command\DeleteByPathInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\Processor as ProcessorSubject; +use Psr\Log\LoggerInterface; + +/** + * Ensures that metadata is removed from the database when a product image has been deleted. + */ +class Processor +{ + /** + * @var DeleteByPathInterface + */ + private $deleteMediaAssetByPath; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Processor constructor. + * + * @param DeleteByPathInterface $deleteMediaAssetByPath + * @param LoggerInterface $logger + */ + public function __construct( + DeleteByPathInterface $deleteMediaAssetByPath, + LoggerInterface $logger + ) { + $this->deleteMediaAssetByPath = $deleteMediaAssetByPath; + $this->logger = $logger; + } + + /** + * Remove media asset image after the product gallery image remove + * + * @param ProcessorSubject $subject + * @param ProcessorSubject $result + * @param Product $product + * @param string $file + * + * @return ProcessorSubject + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterRemoveImage( + ProcessorSubject $subject, + ProcessorSubject $result, + Product $product, + $file + ): ProcessorSubject { + if (!is_string($file)) { + return $result; + } + + try { + $this->deleteMediaAssetByPath->execute($file); + } catch (\Exception $exception) { + $this->logger->critical($exception); + } + + return $result; + } +} diff --git a/app/code/Magento/MediaGallery/Plugin/Wysiwyg/Images/Storage.php b/app/code/Magento/MediaGallery/Plugin/Wysiwyg/Images/Storage.php new file mode 100644 index 0000000000000..ff0e1528e0597 --- /dev/null +++ b/app/code/Magento/MediaGallery/Plugin/Wysiwyg/Images/Storage.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallery\Plugin\Wysiwyg\Images; + +use Magento\MediaGalleryApi\Model\Asset\Command\GetByPathInterface; +use Magento\MediaGalleryApi\Model\Asset\Command\DeleteByPathInterface; +use Magento\Cms\Model\Wysiwyg\Images\Storage as StorageSubject; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Exception\ValidatorException; +use Psr\Log\LoggerInterface; + +/** + * Ensures that metadata is removed from the database when a file is deleted and it is an image + */ +class Storage +{ + /** + * @var GetByPathInterface + */ + private $getMediaAssetByPath; + + /** + * @var DeleteByPathInterface + */ + private $deleteMediaAssetByPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Storage constructor. + * + * @param GetByPathInterface $getMediaAssetByPath + * @param DeleteByPathInterface $deleteMediaAssetByPath + * @param Filesystem $filesystem + * @param LoggerInterface $logger + */ + public function __construct( + GetByPathInterface $getMediaAssetByPath, + DeleteByPathInterface $deleteMediaAssetByPath, + Filesystem $filesystem, + LoggerInterface $logger + ) { + $this->getMediaAssetByPath = $getMediaAssetByPath; + $this->deleteMediaAssetByPath = $deleteMediaAssetByPath; + $this->filesystem = $filesystem; + $this->logger = $logger; + } + + /** + * Delete media data after the image delete action from Wysiwyg + * + * @param StorageSubject $subject + * @param StorageSubject $result + * @param string $target + * + * @return StorageSubject + * @throws ValidatorException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDeleteFile(StorageSubject $subject, StorageSubject $result, $target): StorageSubject + { + if (!is_string($target)) { + return $result; + } + + $relativePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getRelativePath($target); + if (!$relativePath) { + return $result; + } + + try { + $this->deleteMediaAssetByPath->execute($relativePath); + } catch (\Exception $exception) { + $this->logger->critical($exception); + } + + return $result; + } +} diff --git a/app/code/Magento/MediaGallery/README.md b/app/code/Magento/MediaGallery/README.md new file mode 100644 index 0000000000000..ed97b5df98fc2 --- /dev/null +++ b/app/code/Magento/MediaGallery/README.md @@ -0,0 +1,23 @@ +# Magento_MediaGallery module + +The Magento_MediaGallery module is responsible for storing and managing media gallery assets attributes. + +## Installation details + +The Magento_MediaGallery module creates the following tables in the database: + +- `media_gallery_asset` +- `media_gallery_keyword` +- `media_gallery_asset_keyword` + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Extensibility + +Extension developers can interact with the Magento_MediaGallery module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallery module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/DeleteByPathTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/DeleteByPathTest.php new file mode 100644 index 0000000000000..e6de7232ac153 --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/DeleteByPathTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Asset\Command; + +use Magento\MediaGallery\Model\Asset\Command\DeleteByPath; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Exception\CouldNotDeleteException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Psr\Log\LoggerInterface; + +/** + * Test the DeleteByPath command model + */ +class DeleteByPathTest extends TestCase +{ + private const TABLE_NAME = 'media_gallery_asset'; + private const FILE_PATH = 'test-file-path/test.jpg'; + + /** + * @var DeleteByPath + */ + private $deleteMediaAssetByPath; + + /** + * @var AdapterInterface|MockObject + */ + private $adapter; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * Initialize basic test class mocks + */ + protected function setUp(): void + { + $this->logger = $this->createMock(LoggerInterface::class); + $resourceConnection = $this->createMock(ResourceConnection::class); + + $this->deleteMediaAssetByPath = (new ObjectManager($this))->getObject( + DeleteByPath::class, + [ + 'resourceConnection' => $resourceConnection, + 'logger' => $this->logger, + ] + ); + + $this->adapter = $this->createMock(AdapterInterface::class); + $resourceConnection->expects($this->once()) + ->method('getConnection') + ->willReturn($this->adapter); + $resourceConnection->expects($this->once()) + ->method('getTableName') + ->with(self::TABLE_NAME) + ->willReturn('prefix_' . self::TABLE_NAME); + } + + /** + * Test delete media asset by path command + */ + public function testSuccessfulDeleteByIdExecution(): void + { + $this->adapter->expects($this->once()) + ->method('delete') + ->with('prefix_' . self::TABLE_NAME, ['path = ?' => self::FILE_PATH]); + + $this->deleteMediaAssetByPath->execute(self::FILE_PATH); + } + + /** + * Assume that delete action will thrown an Exception + */ + public function testExceptionOnDeleteExecution(): void + { + $this->adapter->expects($this->once()) + ->method('delete') + ->with('prefix_' . self::TABLE_NAME, ['path = ?' => self::FILE_PATH]) + ->willThrowException(new \Exception()); + + $this->expectException(CouldNotDeleteException::class); + $this->logger->expects($this->once()) + ->method('critical') + ->willReturnSelf(); + $this->deleteMediaAssetByPath->execute(self::FILE_PATH); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php new file mode 100644 index 0000000000000..0d0bfc510b518 --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Asset\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGallery\Model\Asset\Command\GetById; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Zend\Db\Adapter\Driver\Pdo\Statement; + +/** + * Test the GetById command with exception during media asset initialization + */ +class GetByIdExceptionDuringMediaAssetInitializationTest extends \PHPUnit\Framework\TestCase +{ + private const MEDIA_ASSET_STUB_ID = 1; + + private const MEDIA_ASSET_DATA = ['id' => 1]; + + /** + * @var GetById|MockObject + */ + private $getMediaAssetById; + + /** + * @var AssetInterfaceFactory|MockObject + */ + private $assetFactory; + + /** + * @var AdapterInterface|MockObject + */ + private $adapter; + + /** + * @var Select|MockObject + */ + private $selectStub; + + /** + * @var Statement|MockObject + */ + private $statementMock; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * Initialize basic test class mocks + */ + protected function setUp(): void + { + $resourceConnection = $this->createMock(ResourceConnection::class); + $this->assetFactory = $this->createMock(AssetInterfaceFactory::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->getMediaAssetById = (new ObjectManager($this))->getObject( + GetById::class, + [ + 'resourceConnection' => $resourceConnection, + 'assetFactory' => $this->assetFactory, + 'logger' => $this->logger, + ] + ); + $this->adapter = $this->createMock(AdapterInterface::class); + $resourceConnection->method('getConnection')->willReturn($this->adapter); + + $this->selectStub = $this->createMock(Select::class); + $this->selectStub->method('from')->willReturnSelf(); + $this->selectStub->method('where')->willReturnSelf(); + $this->adapter->method('select')->willReturn($this->selectStub); + + $this->statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class)->getMock(); + } + + /** + * Test case when a problem occurred during asset initialization from received data. + */ + public function testErrorDuringMediaAssetInitializationException(): void + { + $this->statementMock->method('fetch')->willReturn(self::MEDIA_ASSET_DATA); + $this->adapter->method('query')->willReturn($this->statementMock); + + $this->assetFactory->expects($this->once())->method('create')->willThrowException(new \Exception()); + + $this->expectException(IntegrationException::class); + $this->logger->expects($this->any()) + ->method('critical') + ->willReturnSelf(); + + $this->getMediaAssetById->execute(self::MEDIA_ASSET_STUB_ID); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionNoSuchEntityTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionNoSuchEntityTest.php new file mode 100644 index 0000000000000..0ca9b3a3ffc8a --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionNoSuchEntityTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Asset\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGallery\Model\Asset\Command\GetById; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Zend\Db\Adapter\Driver\Pdo\Statement; + +/** + * Test the GetById command with exception thrown in case when there is no such entity + */ +class GetByIdExceptionNoSuchEntityTest extends \PHPUnit\Framework\TestCase +{ + private const MEDIA_ASSET_STUB_ID = 1; + + /** + * @var GetById|MockObject + */ + private $getMediaAssetById; + + /** + * @var AssetInterfaceFactory|MockObject + */ + private $assetFactory; + + /** + * @var AdapterInterface|MockObject + */ + private $adapter; + + /** + * @var Select|MockObject + */ + private $selectStub; + + /** + * @var Statement|MockObject + */ + private $statementMock; + + /** + * Initialize basic test class mocks + */ + protected function setUp(): void + { + $resourceConnection = $this->createMock(ResourceConnection::class); + $this->assetFactory = $this->createMock(AssetInterfaceFactory::class); + $logger = $this->createMock(LoggerInterface::class); + + $this->getMediaAssetById = (new ObjectManager($this))->getObject( + GetById::class, + [ + 'resourceConnection' => $resourceConnection, + 'assetFactory' => $this->assetFactory, + 'logger' => $logger, + ] + ); + $this->adapter = $this->createMock(AdapterInterface::class); + $resourceConnection->method('getConnection')->willReturn($this->adapter); + + $this->selectStub = $this->createMock(Select::class); + $this->selectStub->method('from')->willReturnSelf(); + $this->selectStub->method('where')->willReturnSelf(); + $this->adapter->method('select')->willReturn($this->selectStub); + + $this->statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class)->getMock(); + } + + /** + * Test case when there is no found media asset by id. + */ + public function testNotFoundMediaAssetException(): void + { + $this->statementMock->method('fetch')->willReturn([]); + $this->adapter->method('query')->willReturn($this->statementMock); + + $this->expectException(NoSuchEntityException::class); + + $this->getMediaAssetById->execute(self::MEDIA_ASSET_STUB_ID); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php new file mode 100644 index 0000000000000..a709c2d214bda --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Asset\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGallery\Model\Asset\Command\GetById; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Zend\Db\Adapter\Driver\Pdo\Statement; + +/** + * Test the GetById command with exception during get media data + */ +class GetByIdExceptionOnGetDataTest extends \PHPUnit\Framework\TestCase +{ + private const MEDIA_ASSET_STUB_ID = 1; + + private const MEDIA_ASSET_DATA = ['id' => 1]; + + /** + * @var GetById|MockObject + */ + private $getMediaAssetById; + /** + * @var AdapterInterface|MockObject + */ + private $adapter; + + /** + * @var Select|MockObject + */ + private $selectStub; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Statement|MockObject + */ + private $statementMock; + + /** + * Initialize basic test class mocks + */ + protected function setUp(): void + { + $resourceConnection = $this->createMock(ResourceConnection::class); + $assetFactory = $this->createMock(AssetInterfaceFactory::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->getMediaAssetById = (new ObjectManager($this))->getObject( + GetById::class, + [ + 'resourceConnection' => $resourceConnection, + 'assetFactory' => $assetFactory, + 'logger' => $this->logger, + ] + ); + $this->adapter = $this->createMock(AdapterInterface::class); + $resourceConnection->method('getConnection')->willReturn($this->adapter); + + $this->selectStub = $this->createMock(Select::class); + $this->selectStub->method('from')->willReturnSelf(); + $this->selectStub->method('where')->willReturnSelf(); + $this->adapter->method('select')->willReturn($this->selectStub); + + $this->statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class)->getMock(); + } + + /** + * Test an exception during the get asset data query. + */ + public function testExceptionDuringGetMediaAssetData(): void + { + $this->statementMock->method('fetch')->willReturn(self::MEDIA_ASSET_DATA); + $this->adapter->method('query')->willThrowException(new \Exception()); + + $this->expectException(IntegrationException::class); + $this->logger->expects($this->any()) + ->method('critical') + ->willReturnSelf(); + + $this->getMediaAssetById->execute(self::MEDIA_ASSET_STUB_ID); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php new file mode 100644 index 0000000000000..c300d4f121bd2 --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Asset\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGallery\Model\Asset\Command\GetById; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Zend\Db\Adapter\Driver\Pdo\Statement; + +/** + * Test the GetById command successful scenario + */ +class GetByIdSuccessfulTest extends \PHPUnit\Framework\TestCase +{ + private const MEDIA_ASSET_STUB_ID = 1; + + private const MEDIA_ASSET_DATA = ['id' => 1]; + + /** + * @var GetById|MockObject + */ + private $getMediaAssetById; + + /** + * @var AssetInterfaceFactory|MockObject + */ + private $assetFactory; + + /** + * @var AdapterInterface|MockObject + */ + private $adapter; + + /** + * @var Select|MockObject + */ + private $selectStub; + + /** + * @var Statement|MockObject + */ + private $statementMock; + + /** + * Initialize basic test class mocks + */ + protected function setUp(): void + { + $resourceConnection = $this->createMock(ResourceConnection::class); + $this->assetFactory = $this->createMock(AssetInterfaceFactory::class); + $logger = $this->createMock(LoggerInterface::class); + + $this->getMediaAssetById = (new ObjectManager($this))->getObject( + GetById::class, + [ + 'resourceConnection' => $resourceConnection, + 'assetFactory' => $this->assetFactory, + 'logger' => $logger, + ] + ); + $this->adapter = $this->createMock(AdapterInterface::class); + $resourceConnection->method('getConnection')->willReturn($this->adapter); + + $this->selectStub = $this->createMock(Select::class); + $this->selectStub->method('from')->willReturnSelf(); + $this->selectStub->method('where')->willReturnSelf(); + $this->adapter->method('select')->willReturn($this->selectStub); + + $this->statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class)->getMock(); + } + + /** + * Test successful get media asset by id command execution. + */ + public function testSuccessfulGetByIdExecution(): void + { + $this->statementMock->method('fetch')->willReturn(self::MEDIA_ASSET_DATA); + $this->adapter->method('query')->willReturn($this->statementMock); + + $mediaAssetStub = $this->getMockBuilder(AssetInterface::class)->getMock(); + $this->assetFactory->expects($this->once())->method('create')->willReturn($mediaAssetStub); + + $this->assertEquals( + $mediaAssetStub, + $this->getMediaAssetById->execute(self::MEDIA_ASSET_STUB_ID) + ); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/SaveTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/SaveTest.php new file mode 100644 index 0000000000000..2f736fb832eac --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/SaveTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Asset\Command; + +use Magento\MediaGallery\Model\Asset\Command\Save; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Model\DataExtractorInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Tests the Save model using PHPUnit + */ +class SaveTest extends TestCase +{ + + /** + * Constant for tablename of media gallery assets + */ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + /** + * Constant for prefixed tablename of media gallery assets + */ + private const PREFIXED_TABLE_MEDIA_GALLERY_ASSET = 'prefix_' . self::TABLE_MEDIA_GALLERY_ASSET; + + /** + * Constant for last ID generated after data insertion + */ + private const INSERT_ID = '1'; + + /** + * Constant for affected rows count after data insertion + */ + private const AFFECTED_ROWS = 1; + + /** + * Constant for image data + */ + private const IMAGE_DATA = [ + 'path' => '/test/path', + 'title' => 'Test Title', + 'source' => 'Adobe Stock', + 'content_type' => 'image/jpeg', + 'height' => 4863, + 'width' => 12129 + ]; + + /** + * @var MockObject | ResourceConnection + */ + private $resourceConnectionMock; + + /** + * @var MockObject | DataExtractorInterface + */ + private $loggerMock; + + /** + * @var MockObject | LoggerInterface + */ + private $extractorMock; + + /** + * @var MockObject | AdapterInterface + */ + private $adapterMock; + + /** + * @var MockObject | AssetInterface + */ + private $mediaAssetMock; + + /** + * @var Save + */ + private $save; + + /** + * Set up test mocks + */ + protected function setUp(): void + { + /* Intermediary mocks */ + $this->adapterMock = $this->createMock(Mysql::class); + $this->mediaAssetMock = $this->createMock(AssetInterface::class); + + /* Save constructor mocks */ + $this->extractorMock = $this->createMock(DataExtractorInterface::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->resourceConnectionMock = $this->createConfiguredMock( + ResourceConnection::class, + [ + 'getConnection' => $this->adapterMock, + 'getTableName' => self::PREFIXED_TABLE_MEDIA_GALLERY_ASSET + ] + ); + + /* Create Save instance with mocks */ + $this->save = (new ObjectManager($this))->getObject( + Save::class, + [ + 'resourceConnection' => $this->resourceConnectionMock, + 'extractor' => $this->extractorMock, + 'logger' => $this->loggerMock + ] + ); + } + + /** + * Tests a successful Save::execute method + */ + public function testSuccessfulExecute(): void + { + $this->resourceConnectionMock->expects(self::once())->method('getConnection'); + $this->resourceConnectionMock->expects(self::once())->method('getTableName'); + + $this->extractorMock + ->expects(self::once()) + ->method('extract') + ->with($this->mediaAssetMock, AssetInterface::class) + ->willReturn(self::IMAGE_DATA); + + $this->adapterMock + ->expects(self::once()) + ->method('insertOnDuplicate') + ->with(self::PREFIXED_TABLE_MEDIA_GALLERY_ASSET, self::IMAGE_DATA) + ->willReturn(self::AFFECTED_ROWS); + + $this->adapterMock + ->expects(self::once()) + ->method('lastInsertId') + ->with(self::PREFIXED_TABLE_MEDIA_GALLERY_ASSET) + ->willReturn(self::INSERT_ID); + + $this->save->execute($this->mediaAssetMock); + } + + /** + * Tests Save::execute method with an exception thrown + */ + public function testExceptionExecute(): void + { + $this->resourceConnectionMock->expects(self::once())->method('getConnection'); + $this->resourceConnectionMock->expects(self::once())->method('getTableName'); + + $this->extractorMock + ->expects(self::once()) + ->method('extract') + ->with($this->mediaAssetMock, AssetInterface::class) + ->willReturn(self::IMAGE_DATA); + + $this->adapterMock + ->expects(self::once()) + ->method('insertOnDuplicate') + ->with(self::PREFIXED_TABLE_MEDIA_GALLERY_ASSET, self::IMAGE_DATA) + ->willThrowException(new \Zend_Db_Exception()); + + $this->loggerMock + ->expects(self::once()) + ->method('critical') + ->willReturnSelf(); + + $this->expectException(CouldNotSaveException::class); + + $this->save->execute($this->mediaAssetMock); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/DataExtractorTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/DataExtractorTest.php new file mode 100644 index 0000000000000..7d57f32449f56 --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/DataExtractorTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGallery\Model\Asset; +use Magento\MediaGallery\Model\Keyword; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGallery\Model\DataExtractor; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class DataExtractorTest extends TestCase +{ + /** + * @var DataExtractor|MockObject + */ + private $dataExtractor; + + /** + * Initialize basic test class mocks + */ + protected function setUp(): void + { + $this->dataExtractor = new DataExtractor(); + } + + /** + * Test extract object data by interface + * + * @dataProvider assetProvider + * + * @param string $class + * @param string|null $interfaceClass + * @param array $expectedData + * + * @throws \ReflectionException + */ + public function testExtractData(string $class, $interfaceClass, array $expectedData): void + { + $data = []; + foreach ($expectedData as $expectedDataKey => $expectedDataItem) { + $data[$expectedDataKey] = $expectedDataItem['value']; + } + $model = (new ObjectManager($this))->getObject( + $class, + [ + 'data' => $data, + ] + ); + $receivedData = $this->dataExtractor->extract($model, $interfaceClass); + $this->checkValues($expectedData, $receivedData, $model); + } + + /** + * @param array $expectedData + * @param array $data + * @param object $model + */ + protected function checkValues(array $expectedData, array $data, $model) + { + foreach ($expectedData as $expectedDataKey => $expectedDataItem) { + $this->assertEquals($data[$expectedDataKey] ?? null, $model->{$expectedDataItem['method']}()); + $this->assertEquals($data[$expectedDataKey] ?? null, $expectedDataItem['value']); + } + $this->assertEquals(array_keys($expectedData), array_keys($expectedData)); + } + + /** + * @return array + */ + public function assetProvider() + { + return [ + 'Asset conversion with interface' => [ + Asset::class, + AssetInterface::class, + [ + 'id' => [ + 'value' => 2, + 'method' => 'getId', + ], + 'path' => [ + 'value' => 'path', + 'method' => 'getPath', + ], + 'title' => [ + 'value' => 'title', + 'method' => 'getTitle', + ], + 'source' => [ + 'value' => 'source', + 'method' => 'getSource', + ], + 'content_type' => [ + 'value' => 'content_type', + 'method' => 'getContentType', + ], + 'width' => [ + 'value' => 3, + 'method' => 'getWidth', + ], + 'height' => [ + 'value' => 4, + 'method' => 'getHeight', + ], + 'created_at' => [ + 'value' => '2019-11-28 10:40:09', + 'method' => 'getCreatedAt', + ], + 'updated_at' => [ + 'value' => '2019-11-28 10:41:08', + 'method' => 'getUpdatedAt', + ], + ], + ], + 'Keyword conversion without interface' => [ + Keyword::class, + null, + [ + 'id' => [ + 'value' => 2, + 'method' => 'getId', + ], + 'keyword' => [ + 'value' => 'keyword', + 'method' => 'getKeyword', + ], + ], + ] + ]; + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/GetAssetKeywordsTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/GetAssetKeywordsTest.php new file mode 100644 index 0000000000000..2ccac4eac8343 --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/GetAssetKeywordsTest.php @@ -0,0 +1,150 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Keyword\Command; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGallery\Model\Keyword\Command\GetAssetKeywords; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * GetAssetKeywordsTest + */ +class GetAssetKeywordsTest extends TestCase +{ + /** + * @var GetAssetKeywords + */ + private $sut; + + /** + * @var ResourceConnection | MockObject + */ + private $resourceConnectionStub; + + /** + * @var KeywordInterfaceFactory | MockObject + */ + private $assetKeywordFactoryStub; + + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + + protected function setUp(): void + { + $this->resourceConnectionStub = $this->createMock(ResourceConnection::class); + $this->assetKeywordFactoryStub = $this->createMock(KeywordInterfaceFactory::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->sut = new GetAssetKeywords( + $this->resourceConnectionStub, + $this->assetKeywordFactoryStub, + $this->loggerMock + ); + } + + /** + * Posive test for the main case + * + * @dataProvider casesProvider() + * @param array $databaseQueryResult + * @param int $expectedNumberOfFoundKeywords + */ + public function testFind(array $databaseQueryResult, int $expectedNumberOfFoundKeywords): void + { + $randomAssetId = 12345; + $this->configureResourceConnectionStub($databaseQueryResult); + $this->configureAssetKeywordFactoryStub(); + + /** @var KeywordInterface[] $keywords */ + $keywords = $this->sut->execute($randomAssetId); + + $this->assertCount($expectedNumberOfFoundKeywords, $keywords); + } + + /** + * Data provider for testFind + * + * @return array + */ + public function casesProvider(): array + { + return [ + 'not_found' => [[],0], + 'find_one_keyword' => [['keywordRawData'],1], + 'find_several_keywords' => [['keywordRawData', 'keywordRawData'],2], + ]; + } + + /** + * Test case when an error occured during get data request. + * + * @throws IntegrationException + */ + public function testNotFoundBecauseOfError(): void + { + $randomAssetId = 1; + + $this->resourceConnectionStub + ->method('getConnection') + ->willThrowException((new \Exception())); + + $this->expectException(IntegrationException::class); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->willReturnSelf(); + + $this->sut->execute($randomAssetId); + } + + /** + * Very fragile and coupled to the implementation + * + * @param array $queryResult + */ + private function configureResourceConnectionStub(array $queryResult): void + { + $statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class)->getMock(); + $statementMock + ->method('fetchAll') + ->willReturn($queryResult); + + $selectStub = $this->createMock(Select::class); + $selectStub->method('from')->willReturnSelf(); + $selectStub->method('join')->willReturnSelf(); + $selectStub->method('where')->willReturnSelf(); + + $connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMock(); + $connectionMock + ->method('select') + ->willReturn($selectStub); + $connectionMock + ->method('query') + ->willReturn($statementMock); + + $this->resourceConnectionStub + ->method('getConnection') + ->willReturn($connectionMock); + } + + private function configureAssetKeywordFactoryStub(): void + { + $keywordStub = $this->getMockBuilder(KeywordInterface::class)->getMock(); + $this->assetKeywordFactoryStub + ->method('create') + ->willReturn($keywordStub); + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/SaveAssetKeywordsTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/SaveAssetKeywordsTest.php new file mode 100644 index 0000000000000..a55c60024c08d --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/SaveAssetKeywordsTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Keyword\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\MediaGallery\Model\Keyword\Command\SaveAssetKeywords; +use Magento\MediaGallery\Model\Keyword\Command\SaveAssetLinks; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * SaveAssetKeywordsTest. + */ +class SaveAssetKeywordsTest extends TestCase +{ + /** + * @var SaveAssetKeywords + */ + private $sut; + + /** + * @var ResourceConnection|MockObject + */ + private $resourceConnectionMock; + + /** + * @var Mysql|MockObject + */ + private $connectionMock; + + /** + * @var SaveAssetLinks|MockObject + */ + private $saveAssetLinksMock; + + /** + * @var Select|MockObject + */ + private $selectMock; + + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + + /** + * SetUp + */ + public function setUp(): void + { + $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->saveAssetLinksMock = $this->createMock(SaveAssetLinks::class); + $this->connectionMock = $this->getMockBuilder(Mysql::class) + ->disableOriginalConstructor() + ->getMock(); + $this->selectMock = $this->createMock(Select::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->sut = new SaveAssetKeywords( + $this->resourceConnectionMock, + $this->saveAssetLinksMock, + $this->loggerMock + ); + } + + /** + * Test saving the asset keywords + * + * @dataProvider assetKeywordsDataProvider + * + * @param array $keywords + * @param int $assetId + * @param array $items + */ + public function testAssetKeywordsSave(array $keywords, int $assetId, array $items): void + { + $expectedCalls = (int) (count($keywords)); + + if ($expectedCalls) { + $this->prepareResourceConnection(); + $this->connectionMock->expects($this->once()) + ->method('insertArray') + ->with( + 'prefix_media_gallery_keyword', + ['keyword'], + $items, + 2 + ); + } + + $this->sut->execute($keywords, $assetId); + } + + /** + * Testing throwing exception handling + * + * @throws CouldNotSaveException + */ + public function testAssetNotSavingCausedByError(): void + { + $keyword = new DataObject(['keyword' => 'keyword-1']); + + $this->resourceConnectionMock + ->method('getConnection') + ->willThrowException((new \Exception())); + $this->expectException(CouldNotSaveException::class); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->willReturnSelf(); + + $this->sut->execute([$keyword], 1); + } + + /** + * Preparing the resource connection + */ + private function prepareResourceConnection(): void + { + $this->selectMock->method('from')->willReturnSelf(); + $this->selectMock->method('columns')->with('id')->willReturnSelf(); + $this->selectMock->method('where')->willReturnSelf(); + + $this->connectionMock + ->method('select') + ->willReturn($this->selectMock); + $this->connectionMock + ->method('fetchCol') + ->willReturn([['id'=> 1], ['id' => 2]]); + $this->resourceConnectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->resourceConnectionMock->expects($this->any()) + ->method('getTableName') + ->with('media_gallery_keyword') + ->willReturn('prefix_media_gallery_keyword'); + } + + /** + * Providing asset keywords + * + * @return array + */ + public function assetKeywordsDataProvider(): array + { + return [ + [ + [], + 1, + [] + ], [ + [ + new DataObject(['keyword' => 'keyword-1']), + new DataObject(['keyword' => 'keyword-2']), + ], + 1, + ['keyword-1', 'keyword-2'] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/SaveAssetLinksTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/SaveAssetLinksTest.php new file mode 100644 index 0000000000000..2981c534586e2 --- /dev/null +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Keyword/Command/SaveAssetLinksTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Test\Unit\Model\Keyword\Command; + +use Magento\MediaGallery\Model\Keyword\Command\SaveAssetLinks; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Exception\CouldNotSaveException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * SaveAssetLinksTest. + */ +class SaveAssetLinksTest extends TestCase +{ + /** + * @var SaveAssetLinks + */ + private $sut; + + /** + * @var AdapterInterface|MockObject + */ + private $connectionMock; + + /** + * @var ResourceConnection|MockObject + */ + private $resourceConnectionMock; + + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + + /** + * Prepare test objects. + */ + public function setUp(): void + { + $this->connectionMock = $this->createMock(AdapterInterface::class); + $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->sut = new SaveAssetLinks( + $this->resourceConnectionMock, + $this->loggerMock + ); + } + + /** + * Test saving the asset keyword links + * + * @dataProvider assetLinksDataProvider + * + * @param int $assetId + * @param array $keywordIds + * @param array $values + */ + public function testAssetKeywordsSave(int $assetId, array $keywordIds, array $values): void + { + $expectedCalls = (int) (count($keywordIds)); + + if ($expectedCalls) { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->with('media_gallery_asset_keyword') + ->willReturn('prefix_media_gallery_asset_keyword'); + $this->connectionMock->expects($this->once()) + ->method('insertArray') + ->with( + 'prefix_media_gallery_asset_keyword', + ['asset_id', 'keyword_id'], + $values, + 2 + ); + } + + $this->sut->execute($assetId, $keywordIds); + } + + /** + * Testing throwing exception handling + * + * @throws CouldNotSaveException + */ + public function testAssetNotSavingCausedByError(): void + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once()) + ->method('insertArray') + ->willThrowException((new \Exception())); + $this->expectException(CouldNotSaveException::class); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->willReturnSelf(); + + $this->sut->execute(1, [1, 2]); + } + + /** + * Providing asset links + * + * @return array + */ + public function assetLinksDataProvider(): array + { + return [ + [ + 12, + [], + [] + ], + [ + 12, + [1], + [ + [12, 1] + ] + ], [ + 12, + [1, 2], + [ + [12, 1], + [12, 2], + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGallery/composer.json b/app/code/Magento/MediaGallery/composer.json new file mode 100644 index 0000000000000..977277d993061 --- /dev/null +++ b/app/code/Magento/MediaGallery/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery", + "description": "Magento module responsible for media handling", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-cms": "*", + "magento/module-catalog": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallery\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml new file mode 100644 index 0000000000000..fac1342528f2c --- /dev/null +++ b/app/code/Magento/MediaGallery/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="media_gallery_asset" resource="default" engine="innodb" comment="Media Gallery Asset"> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> + <column xsi:type="varchar" name="path" length="255" nullable="true" comment="Path"/> + <column xsi:type="varchar" name="title" length="255" nullable="true" comment="Title"/> + <column xsi:type="varchar" name="source" length="255" nullable="true" comment="Source"/> + <column xsi:type="varchar" name="content_type" length="255" nullable="true" comment="Content Type"/> + <column xsi:type="int" name="width" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Width"/> + <column xsi:type="int" name="height" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Height"/> + <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="id"/> + </constraint> + <index referenceId="MEDIA_GALLERY_ID" indexType="btree"> + <column name="id"/> + </index> + <constraint xsi:type="unique" referenceId="MEDIA_GALLERY_ID_PATH_TITLE_CONTENT_TYPE_WIDTH_HEIGHT"> + <column name="path"/> + </constraint> + </table> + <table name="media_gallery_keyword" resource="default" engine="innodb" comment="Media Gallery Keyword"> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Keyword ID"/> + <column xsi:type="varchar" length="255" name="keyword" nullable="false" comment="Keyword"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="id"/> + </constraint> + <index referenceId="MEDIA_GALLERY_KEYWORD" indexType="btree"> + <column name="id"/> + </index> + <constraint xsi:type="unique" referenceId="MEDIA_GALLERY_KEYWORD_KEYWORD_UNIQUE"> + <column name="keyword"/> + </constraint> + </table> + <table name="media_gallery_asset_keyword" resource="default" engine="innodb" comment="Media Gallery Asset Keyword"> + <column xsi:type="int" name="keyword_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Keyword Id"/> + <column xsi:type="int" name="asset_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Asset ID"/> + <index referenceId="MEDIA_GALLERY_ASSET_KEYWORD_ASSET_ID_INDEX" indexType="btree"> + <column name="asset_id"/> + </index> + <index referenceId="MEDIA_GALLERY_ASSET_KEYWORD_KEYWORD_ID_INDEX" indexType="btree"> + <column name="keyword_id"/> + </index> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="keyword_id"/> + <column name="asset_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MEDIA_GALLERY_KEYWORD_KEYWORD_ID_MEDIA_GALLERY_KEYWORD_ID" table="media_gallery_asset_keyword" column="keyword_id" referenceTable="media_gallery_keyword" referenceColumn="id" onDelete="CASCADE"/> + <constraint xsi:type="foreign" referenceId="MEDIA_GALLERY_KEYWORD_ASSET_ID_ASSET_ID" table="media_gallery_asset_keyword" column="asset_id" referenceTable="media_gallery_asset" referenceColumn="id" onDelete="CASCADE"/> + </table> +</schema> diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..10db10d5dd5db --- /dev/null +++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json @@ -0,0 +1,56 @@ +{ + "media_gallery_asset": { + "column": { + "id": true, + "path": true, + "title": true, + "source": true, + "content_type": true, + "width": true, + "height": true, + "created_at": true, + "updated_at": true + }, + "index": { + "MEDIA_GALLERY_ID": true, + "MEDIA_GALLERY_ASSET_ID": true + }, + "constraint": { + "MEDIA_GALLERY_ID_PATH_TITLE_CONTENT_TYPE_WIDTH_HEIGHT": true, + "PRIMARY": true, + "MEDIA_GALLERY_ASSET_PATH": true + } + }, + "media_gallery_keyword": { + "column": { + "id": true, + "keyword": true + }, + "index": { + "MEDIA_GALLERY_KEYWORD_ID": true + }, + "constraint": { + "MEDIA_GALLERY_KEYWORD_KEYWORD": true, + "PRIMARY": true + } + }, + "media_gallery_asset_keyword": { + "column": { + "keyword_id": true, + "asset_id": true + }, + "index": { + "MEDIA_GALLERY_ASSET_KEYWORD_ASSET_ID_INDEX": true, + "MEDIA_GALLERY_ASSET_KEYWORD_KEYWORD_ID_INDEX": true, + "MEDIA_GALLERY_ASSET_KEYWORD_ASSET_ID": true, + "MEDIA_GALLERY_ASSET_KEYWORD_KEYWORD_ID": true + }, + "constraint": { + "PRIMARY": true, + "MEDIA_GALLERY_KEYWORD_KEYWORD_ID_MEDIA_GALLERY_KEYWORD_ID": true, + "MEDIA_GALLERY_KEYWORD_ASSET_ID_ASSET_ID": true, + "MEDIA_GALLERY_ASSET_KEYWORD_KEYWORD_ID_MEDIA_GALLERY_KEYWORD_ID": true, + "MEDIA_GALLERY_ASSET_KEYWORD_ASSET_ID_MEDIA_GALLERY_ASSET_ID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/MediaGallery/etc/di.xml b/app/code/Magento/MediaGallery/etc/di.xml new file mode 100644 index 0000000000000..8c4a856852e1a --- /dev/null +++ b/app/code/Magento/MediaGallery/etc/di.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:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryApi\Api\Data\KeywordInterface" type="Magento\MediaGallery\Model\Keyword"/> + <preference for="Magento\MediaGalleryApi\Api\Data\AssetInterface" type="Magento\MediaGallery\Model\Asset"/> + + <preference for="Magento\MediaGalleryApi\Model\Asset\Command\GetByIdInterface" type="Magento\MediaGallery\Model\Asset\Command\GetById"/> + <preference for="Magento\MediaGalleryApi\Model\Asset\Command\SaveInterface" type="Magento\MediaGallery\Model\Asset\Command\Save"/> + <preference for="Magento\MediaGalleryApi\Model\Asset\Command\GetByPathInterface" type="Magento\MediaGallery\Model\Asset\Command\GetByPath"/> + <preference for="Magento\MediaGalleryApi\Model\Asset\Command\DeleteByPathInterface" type="Magento\MediaGallery\Model\Asset\Command\DeleteByPath"/> + + <preference for="Magento\MediaGalleryApi\Model\Keyword\Command\GetAssetKeywordsInterface" type="Magento\MediaGallery\Model\Keyword\Command\GetAssetKeywords"/> + <preference for="Magento\MediaGalleryApi\Model\Keyword\Command\SaveAssetKeywordsInterface" type="Magento\MediaGallery\Model\Keyword\Command\SaveAssetKeywords"/> + <preference for="Magento\MediaGalleryApi\Model\Keyword\Command\SaveAssetLinksInterface" type="Magento\MediaGallery\Model\Keyword\Command\SaveAssetLinks"/> + + <preference for="Magento\MediaGalleryApi\Model\DataExtractorInterface" type="Magento\MediaGallery\Model\DataExtractor"/> + + <type name="Magento\Catalog\Model\Product\Gallery\Processor"> + <plugin name="media_gallery_image_remove_metadata" type="Magento\MediaGallery\Plugin\Product\Gallery\Processor" + sortOrder="10" disabled="false"/> + </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <plugin name="media_gallery_image_remove_metadata_after_wysiwyg" type="Magento\MediaGallery\Plugin\Wysiwyg\Images\Storage" + sortOrder="10" disabled="false"/> + </type> +</config> diff --git a/app/code/Magento/MediaGallery/etc/module.xml b/app/code/Magento/MediaGallery/etc/module.xml new file mode 100644 index 0000000000000..bf731d899c15b --- /dev/null +++ b/app/code/Magento/MediaGallery/etc/module.xml @@ -0,0 +1,10 @@ +<?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_MediaGallery"/> +</config> diff --git a/app/code/Magento/MediaGallery/registration.php b/app/code/Magento/MediaGallery/registration.php new file mode 100644 index 0000000000000..a243eee924894 --- /dev/null +++ b/app/code/Magento/MediaGallery/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_MediaGallery', __DIR__); diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php new file mode 100644 index 0000000000000..affae296ca530 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Represents a media gallery asset which contains information about a media asset entity such + * as path to the media storage, media asset title and its content type, etc. + */ +interface AssetInterface extends ExtensibleDataInterface +{ + /** + * Get ID + * + * @return int|null + */ + public function getId(): ?int; + + /** + * Get Path + * + * @return string + */ + public function getPath(): string; + + /** + * Get title + * + * @return string|null + */ + public function getTitle(): ?string; + + /** + * Get source of the file + * + * @return string|null + */ + public function getSource(): ?string; + + /** + * Get content type + * + * @return string + */ + public function getContentType(): string; + + /** + * Retrieve full licensed asset's height + * + * @return int + */ + public function getHeight(): int; + + /** + * Retrieve full licensed asset's width + * + * @return int + */ + public function getWidth(): int; + + /** + * Get created at + * + * @return string + */ + public function getCreatedAt(): string; + + /** + * Get updated at + * + * @return string + */ + public function getUpdatedAt(): string; + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface|null + */ + public function getExtensionAttributes(): AssetExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(AssetExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php new file mode 100644 index 0000000000000..ae3b7dbd76291 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Represents a media gallery keyword. This object contains information about a media asset keyword entity. + */ +interface KeywordInterface extends ExtensibleDataInterface +{ + /** + * Get ID + * + * @return int|null + */ + public function getId(): ?int; + + /** + * Get the keyword + * + * @return string + */ + public function getKeyword(): string; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface|null + */ + public function getExtensionAttributes(): KeywordExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(KeywordExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryApi/LICENSE.txt b/app/code/Magento/MediaGalleryApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php new file mode 100644 index 0000000000000..b3612a67ed536 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model\Asset\Command; + +/** + * A command represents the media gallery asset delete action. A media gallery asset is filtered by path value. + */ +interface DeleteByPathInterface +{ + /** + * Delete media asset by path + * + * @param string $mediaAssetPath + * + * @return void + */ + public function execute(string $mediaAssetPath): void; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php new file mode 100644 index 0000000000000..ef2ceb5ffbfe6 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model\Asset\Command; + +/** + * A command represents the get media gallery asset by using media gallery asset id as a filter parameter. + */ +interface GetByIdInterface +{ + /** + * Get media asset by id + * + * @param int $mediaAssetId + * + * @return \Magento\MediaGalleryApi\Api\Data\AssetInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\IntegrationException + */ + public function execute(int $mediaAssetId): \Magento\MediaGalleryApi\Api\Data\AssetInterface; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php new file mode 100644 index 0000000000000..547b0dc695dae --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model\Asset\Command; + +/** + * A command represents the get media gallery asset by using media gallery asset path as a filter parameter. + */ +interface GetByPathInterface +{ + /** + * Get media asset list + * + * @param string $mediaFilePath + * + * @return \Magento\MediaGalleryApi\Api\Data\AssetInterface + */ + public function execute(string $mediaFilePath): \Magento\MediaGalleryApi\Api\Data\AssetInterface; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php new file mode 100644 index 0000000000000..b3e3607e6e822 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model\Asset\Command; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * A command which executes the media gallery asset save operation. + */ +interface SaveInterface +{ + /** + * Save media asset + * + * @param \Magento\MediaGalleryApi\Api\Data\AssetInterface $mediaAsset + * + * @return int + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function execute(AssetInterface $mediaAsset): int; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/DataExtractorInterface.php b/app/code/Magento/MediaGalleryApi/Model/DataExtractorInterface.php new file mode 100644 index 0000000000000..6570cd2235412 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/DataExtractorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model; + +/** + * Extract data from an object using available getters + */ +interface DataExtractorInterface +{ + /** + * Extract data from an object using available getters (does not process extension attributes) + * + * @param object $object + * @param string|null $interface + * @return array + */ + public function extract($object, string $interface = null): array; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php new file mode 100644 index 0000000000000..d449df5684c4b --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model\Keyword\Command; + +/** + * A command represents functionality to get a media gallery asset keywords filtered by media gallery asset id. + */ +interface GetAssetKeywordsInterface +{ + /** + * Get asset related keywords. + * + * @param int $assetId + * + * @return \Magento\MediaGalleryApi\Api\Data\KeywordInterface[] + */ + public function execute(int $assetId): array; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php new file mode 100644 index 0000000000000..9c0d89c3456f8 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Model\Keyword\Command; + +/** + * A command represents the media gallery asset keywords save operation. + */ +interface SaveAssetKeywordsInterface +{ + /** + * Save asset keywords. + * + * @param \Magento\MediaGalleryApi\Api\Data\KeywordInterface[] $keywords + * @param int $assetId + * @throws \Magento\Framework\Exception\CouldNotSaveException + */ + public function execute(array $keywords, int $assetId): void; +} diff --git a/app/code/Magento/MediaGalleryApi/README.md b/app/code/Magento/MediaGalleryApi/README.md new file mode 100644 index 0000000000000..978a14691597b --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryApi module + +The Magento_MediaGalleryApi module serves as application program interface (API) responsible for storing and managing media gallery asset attributes. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallery module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryApi/composer.json b/app/code/Magento/MediaGalleryApi/composer.json new file mode 100644 index 0000000000000..33ba72c3f98dd --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-api", + "description": "Magento module responsible for media gallery asset attributes storage and management", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryApi/etc/module.xml b/app/code/Magento/MediaGalleryApi/etc/module.xml new file mode 100644 index 0000000000000..36da50a22b8bc --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/etc/module.xml @@ -0,0 +1,10 @@ +<?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_MediaGalleryApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryApi/registration.php b/app/code/Magento/MediaGalleryApi/registration.php new file mode 100644 index 0000000000000..11b0200b46e30 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/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_MediaGalleryApi', __DIR__); diff --git a/app/code/Magento/MediaStorage/Console/Command/ImagesResizeCommand.php b/app/code/Magento/MediaStorage/Console/Command/ImagesResizeCommand.php index ba12d60cb0bc8..4ed84829c2ad0 100644 --- a/app/code/Magento/MediaStorage/Console/Command/ImagesResizeCommand.php +++ b/app/code/Magento/MediaStorage/Console/Command/ImagesResizeCommand.php @@ -8,26 +8,37 @@ namespace Magento\MediaStorage\Console\Command; use Magento\Framework\App\Area; -use Magento\Framework\App\ObjectManager; use Magento\Framework\App\State; -use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Console\Cli; use Magento\MediaStorage\Service\ImageResize; +use Magento\MediaStorage\Service\ImageResizeScheduler; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBarFactory; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Command\Command; +use Magento\Catalog\Model\ResourceModel\Product\Image as ProductImage; /** * Resizes product images according to theme view definitions. - * - * @package Magento\MediaStorage\Console\Command */ -class ImagesResizeCommand extends \Symfony\Component\Console\Command\Command +class ImagesResizeCommand extends Command { + /** + * Asynchronous image resize mode + */ + const ASYNC_RESIZE = 'async'; + + /** + * @var ImageResizeScheduler + */ + private $imageResizeScheduler; + /** * @var ImageResize */ - private $resize; + private $imageResize; /** * @var State @@ -39,24 +50,32 @@ class ImagesResizeCommand extends \Symfony\Component\Console\Command\Command */ private $progressBarFactory; + /** + * @var ProductImage + */ + private $productImage; + /** * @param State $appState - * @param ImageResize $resize - * @param ObjectManagerInterface $objectManager + * @param ImageResize $imageResize + * @param ImageResizeScheduler $imageResizeScheduler * @param ProgressBarFactory $progressBarFactory + * @param ProductImage $productImage * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( State $appState, - ImageResize $resize, - ObjectManagerInterface $objectManager, - ProgressBarFactory $progressBarFactory = null + ImageResize $imageResize, + ImageResizeScheduler $imageResizeScheduler, + ProgressBarFactory $progressBarFactory, + ProductImage $productImage ) { parent::__construct(); - $this->resize = $resize; $this->appState = $appState; - $this->progressBarFactory = $progressBarFactory - ?: ObjectManager::getInstance()->get(ProgressBarFactory::class); + $this->imageResize = $imageResize; + $this->imageResizeScheduler = $imageResizeScheduler; + $this->progressBarFactory = $progressBarFactory; + $this->productImage = $productImage; } /** @@ -65,7 +84,25 @@ public function __construct( protected function configure() { $this->setName('catalog:images:resize') - ->setDescription('Creates resized product images'); + ->setDescription('Creates resized product images') + ->setDefinition($this->getOptionsList()); + } + + /** + * Image resize command options list + * + * @return array + */ + private function getOptionsList() : array + { + return [ + new InputOption( + self::ASYNC_RESIZE, + 'a', + InputOption::VALUE_NONE, + 'Resize image in asynchronous mode' + ), + ]; } /** @@ -74,11 +111,25 @@ protected function configure() * @param OutputInterface $output */ protected function execute(InputInterface $input, OutputInterface $output) + { + $result = $input->getOption(self::ASYNC_RESIZE) ? + $this->executeAsync($output) : $this->executeSync($output); + + return $result; + } + + /** + * Run resize in synchronous mode + * + * @param OutputInterface $output + * @return int + */ + private function executeSync(OutputInterface $output): int { try { $errors = []; $this->appState->setAreaCode(Area::AREA_GLOBAL); - $generator = $this->resize->resizeFromThemes(); + $generator = $this->imageResize->resizeFromThemes(); /** @var ProgressBar $progress */ $progress = $this->progressBarFactory->create( @@ -111,7 +162,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } catch (\Exception $e) { $output->writeln("<error>{$e->getMessage()}</error>"); // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + return Cli::RETURN_FAILURE; } $output->write(PHP_EOL); @@ -124,6 +175,62 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln("<info>Product images resized successfully</info>"); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + return Cli::RETURN_SUCCESS; + } + + /** + * Schedule asynchronous image resizing + * + * @param OutputInterface $output + * @return int + */ + private function executeAsync(OutputInterface $output): int + { + try { + $errors = []; + $this->appState->setAreaCode(Area::AREA_GLOBAL); + + /** @var ProgressBar $progress */ + $progress = $this->progressBarFactory->create( + [ + 'output' => $output, + 'max' => $this->productImage->getCountUsedProductImages() + ] + ); + $progress->setFormat( + "%current%/%max% [%bar%] %percent:3s%% %elapsed% %memory:6s% \t| <info>%message%</info>" + ); + + if ($output->getVerbosity() !== OutputInterface::VERBOSITY_NORMAL) { + $progress->setOverwrite(false); + } + + $productImages = $this->productImage->getUsedProductImages(); + foreach ($productImages as $image) { + $result = $this->imageResizeScheduler->schedule($image['filepath']); + + if (!$result) { + $errors[$image['filepath']] = 'Error image scheduling: ' . $image['filepath']; + } + $progress->setMessage($image['filepath']); + $progress->advance(); + } + } catch (\Exception $e) { + $output->writeln("<error>{$e->getMessage()}</error>"); + // we must have an exit code higher than zero to indicate something was wrong + return Cli::RETURN_FAILURE; + } + + $output->write(PHP_EOL); + if (count($errors)) { + $output->writeln("<info>Product images resized with errors:</info>"); + foreach ($errors as $error) { + $output->writeln("<error>{$error}</error>"); + } + } else { + $output->writeln("<info>Product images scheduled successfully</info>"); + } + + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/MediaStorage/Model/ConsumerImageResize.php b/app/code/Magento/MediaStorage/Model/ConsumerImageResize.php new file mode 100644 index 0000000000000..43f3e1d7767ce --- /dev/null +++ b/app/code/Magento/MediaStorage/Model/ConsumerImageResize.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaStorage\Model; + +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Psr\Log\LoggerInterface; +use Magento\MediaStorage\Service\ImageResize; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Exception\NotFoundException; + +/** + * Consumer for image resize + */ +class ConsumerImageResize +{ + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ImageResize + */ + private $resize; + + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @param ImageResize $resize + * @param LoggerInterface $logger + * @param SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + ImageResize $resize, + LoggerInterface $logger, + SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->resize = $resize; + $this->logger = $logger; + $this->serializer = $serializer; + $this->entityManager = $entityManager; + } + + /** + * Image resize + * + * @param OperationInterface $operation + * @return void + * @throws \Exception + */ + public function process(OperationInterface $operation): void + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->resize->resizeFromImageName($data['filename']); + } catch (NotFoundException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during image resize. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } +} diff --git a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/File.php b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/File.php index 8dfce40419b4a..8c9fe7b848fad 100644 --- a/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/File.php +++ b/app/code/Magento/MediaStorage/Model/ResourceModel/File/Storage/File.php @@ -6,12 +6,20 @@ namespace Magento\MediaStorage\Model\ResourceModel\File\Storage; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\Io\File as FileIo; +use Magento\Framework\App\ObjectManager; /** * Class File */ class File { + /** + * @var FileIo + */ + private $fileIo; + /** * @var \Magento\Framework\Filesystem */ @@ -25,11 +33,16 @@ class File /** * @param \Magento\Framework\Filesystem $filesystem * @param \Psr\Log\LoggerInterface $log + * @param FileIo $fileIo */ - public function __construct(\Magento\Framework\Filesystem $filesystem, \Psr\Log\LoggerInterface $log) - { + public function __construct( + \Magento\Framework\Filesystem $filesystem, + \Psr\Log\LoggerInterface $log, + FileIo $fileIo = null + ) { $this->_logger = $log; $this->_filesystem = $filesystem; + $this->fileIo = $fileIo ?? ObjectManager::getInstance()->get(FileIo::class); } /** @@ -45,14 +58,15 @@ public function getStorageData($dir = '/') $directoryInstance = $this->_filesystem->getDirectoryRead(DirectoryList::MEDIA); if ($directoryInstance->isDirectory($dir)) { foreach ($directoryInstance->readRecursively($dir) as $path) { - $itemName = basename($path); + $pathInfo = $this->fileIo->getPathInfo($path); + $itemName = $pathInfo['basename']; if ($itemName == '.svn' || $itemName == '.htaccess') { continue; } if ($directoryInstance->isDirectory($path)) { $directories[] = [ 'name' => $itemName, - 'path' => dirname($path) == '.' ? '/' : dirname($path), + 'path' => $pathInfo['dirname'] === '.' ? '/' : $pathInfo['dirname'], ]; } else { $files[] = $path; @@ -64,7 +78,7 @@ public function getStorageData($dir = '/') } /** - * Clear files and directories in storage + * Clear all files in storage $dir * * @param string $dir * @return $this @@ -73,8 +87,17 @@ public function clear($dir = '') { $directoryInstance = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); if ($directoryInstance->isDirectory($dir)) { - foreach ($directoryInstance->read($dir) as $path) { - $directoryInstance->delete($path); + $paths = $directoryInstance->readRecursively($dir); + foreach ($paths as $path) { + if ($directoryInstance->isDirectory($path)) { + continue; + } + + $pathInfo = $this->fileIo->getPathInfo($path); + + if ($pathInfo['basename'] !== '.htaccess') { + $directoryInstance->delete($path); + } } } @@ -127,7 +150,7 @@ public function saveFile($filePath, $content, $overwrite = false) } } catch (\Magento\Framework\Exception\FileSystemException $e) { $this->_logger->info($e->getMessage()); - throw new \Magento\Framework\Exception\LocalizedException(__('Unable to save file: %1', $filePath)); + throw new LocalizedException(__('Unable to save file: %1', $filePath)); } return false; diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index 63353b2536a5a..d061ddbd3dc46 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -311,6 +311,10 @@ private function resize(array $imageParams, string $originalImagePath, string $o ] ); + if ($imageParams['image_width'] !== null && $imageParams['image_height'] !== null) { + $image->resize($imageParams['image_width'], $imageParams['image_height']); + } + if (isset($imageParams['watermark_file'])) { if ($imageParams['watermark_height'] !== null) { $image->setWatermarkHeight($imageParams['watermark_height']); @@ -331,9 +335,6 @@ private function resize(array $imageParams, string $originalImagePath, string $o $image->watermark($this->getWatermarkFilePath($imageParams['watermark_file'])); } - if ($imageParams['image_width'] !== null && $imageParams['image_height'] !== null) { - $image->resize($imageParams['image_width'], $imageParams['image_height']); - } $image->save($imageAsset->getPath()); if ($this->fileStorageDatabase->checkDbUsage()) { diff --git a/app/code/Magento/MediaStorage/Service/ImageResizeScheduler.php b/app/code/Magento/MediaStorage/Service/ImageResizeScheduler.php new file mode 100644 index 0000000000000..900bb026dc5b3 --- /dev/null +++ b/app/code/Magento/MediaStorage/Service/ImageResizeScheduler.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaStorage\Service; + +use Magento\Framework\Bulk\BulkManagementInterface; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Authorization\Model\UserContextInterface; + +/** + * Scheduler for image resize queue + */ +class ImageResizeScheduler +{ + /** + * @var BulkManagementInterface + */ + private $bulkManagement; + + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var IdentityGeneratorInterface + */ + private $identityService; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @param BulkManagementInterface $bulkManagement + * @param OperationInterfaceFactory $operartionFactory + * @param IdentityGeneratorInterface $identityService + * @param SerializerInterface $serializer + * @param UserContextInterface $userContext + */ + public function __construct( + BulkManagementInterface $bulkManagement, + OperationInterfaceFactory $operartionFactory, + IdentityGeneratorInterface $identityService, + SerializerInterface $serializer, + UserContextInterface $userContext + ) { + $this->bulkManagement = $bulkManagement; + $this->operationFactory = $operartionFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; + $this->userContext = $userContext; + } + + /** + * Schedule image resize based on original image. + * + * @param string $imageName + * @return boolean + */ + public function schedule(string $imageName): bool + { + $bulkUuid = $this->identityService->generateId(); + $bulkDescription = __('Image resize: %1', $imageName); + $dataToEncode = ['filename' => $imageName]; + + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => 'media.storage.catalog.image.resize', + 'serialized_data' => $this->serializer->serialize($dataToEncode), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + ] + ]; + $operation = $this->operationFactory->create($data); + + return $this->bulkManagement->scheduleBulk( + $bulkUuid, + [$operation], + $bulkDescription, + $this->userContext->getUserId() + ); + } +} diff --git a/app/code/Magento/MediaStorage/Test/Unit/Model/ResourceModel/File/Storage/FileTest.php b/app/code/Magento/MediaStorage/Test/Unit/Model/ResourceModel/File/Storage/FileTest.php index 97dffbe0e39a9..adc045cd0bed5 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/Model/ResourceModel/File/Storage/FileTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/Model/ResourceModel/File/Storage/FileTest.php @@ -6,12 +6,18 @@ namespace Magento\MediaStorage\Test\Unit\Model\ResourceModel\File\Storage; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** * Class FileTest */ class FileTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Filesystem\Io\File + */ + private $fileIoMock; + /** * @var \Magento\MediaStorage\Model\ResourceModel\File\Storage\File */ @@ -44,9 +50,17 @@ protected function setUp() ['isDirectory', 'readRecursively'] ); - $this->storageFile = new \Magento\MediaStorage\Model\ResourceModel\File\Storage\File( - $this->filesystemMock, - $this->loggerMock + $this->fileIoMock = $this->createPartialMock(\Magento\Framework\Filesystem\Io\File::class, ['getPathInfo']); + + $objectManager = new ObjectManager($this); + + $this->storageFile = $objectManager->getObject( + \Magento\MediaStorage\Model\ResourceModel\File\Storage\File::class, + [ + 'filesystem' => $this->filesystemMock, + 'log' => $this->loggerMock, + 'fileIo' => $this->fileIoMock + ] ); } @@ -98,6 +112,20 @@ public function testGetStorageData() 'folder_one/folder_two/.htaccess', 'folder_one/folder_two/file_two.txt', ]; + + $pathInfos = array_map( + function ($path) { + return [$path, pathinfo($path)]; + }, + $paths + ); + + $this->fileIoMock->expects( + $this->any() + )->method( + 'getPathInfo' + )->will($this->returnValueMap($pathInfos)); + sort($paths); $this->directoryReadMock->expects( $this->once() diff --git a/app/code/Magento/MediaStorage/composer.json b/app/code/Magento/MediaStorage/composer.json index 95c48f3fdc581..ce97eec97f7c3 100644 --- a/app/code/Magento/MediaStorage/composer.json +++ b/app/code/Magento/MediaStorage/composer.json @@ -11,7 +11,9 @@ "magento/module-config": "*", "magento/module-store": "*", "magento/module-catalog": "*", - "magento/module-theme": "*" + "magento/module-theme": "*", + "magento/module-asynchronous-operations": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml index 0f6e7f93aea11..2c7219fe8afaa 100644 --- a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml +++ b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml @@ -28,6 +28,7 @@ </field> <field id="configuration_update_time" translate="label" type="text" sortOrder="400" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Environment Update Time</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> </group> </section> diff --git a/app/code/Magento/MediaStorage/etc/communication.xml b/app/code/Magento/MediaStorage/etc/communication.xml new file mode 100644 index 0000000000000..c9630b24ba336 --- /dev/null +++ b/app/code/Magento/MediaStorage/etc/communication.xml @@ -0,0 +1,12 @@ +<?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:Communication/etc/communication.xsd"> + <topic name="media.storage.catalog.image.resize" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="media.storage.catalog.image.resize" type="Magento\MediaStorage\Model\ConsumerImageResize" method="process" /> + </topic> +</config> diff --git a/app/code/Magento/MediaStorage/etc/di.xml b/app/code/Magento/MediaStorage/etc/di.xml index 2b9317787463d..5cdcbb3b2b9a9 100644 --- a/app/code/Magento/MediaStorage/etc/di.xml +++ b/app/code/Magento/MediaStorage/etc/di.xml @@ -26,4 +26,9 @@ </argument> </arguments> </type> + <type name="Magento\MediaStorage\Console\Command\ImagesResizeCommand"> + <arguments> + <argument name="imageResizeScheduler" xsi:type="object">Magento\MediaStorage\Service\ImageResizeScheduler\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaStorage/etc/queue.xml b/app/code/Magento/MediaStorage/etc/queue.xml new file mode 100644 index 0000000000000..3e4ffbf8ec9e2 --- /dev/null +++ b/app/code/Magento/MediaStorage/etc/queue.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/queue.xsd"> + <broker topic="media.storage.catalog.image.resize" exchange="magento-db" type="db"> + <queue name="media.storage.catalog.image.resize" consumer="media.storage.catalog.image.resize" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\MediaStorage\Model\ConsumerImageResize::process" /> + </broker> +</config> diff --git a/app/code/Magento/MediaStorage/etc/queue_consumer.xml b/app/code/Magento/MediaStorage/etc/queue_consumer.xml new file mode 100644 index 0000000000000..e4c06a47f314e --- /dev/null +++ b/app/code/Magento/MediaStorage/etc/queue_consumer.xml @@ -0,0 +1,10 @@ +<?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-message-queue:etc/consumer.xsd"> + <consumer name="media.storage.catalog.image.resize" queue="media.storage.catalog.image.resize" connection="db" maxMessages="100" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\MediaStorage\Model\ConsumerImageResize::process" /> +</config> diff --git a/app/code/Magento/MediaStorage/etc/queue_publisher.xml b/app/code/Magento/MediaStorage/etc/queue_publisher.xml new file mode 100644 index 0000000000000..dab99e2c307f8 --- /dev/null +++ b/app/code/Magento/MediaStorage/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/publisher.xsd"> + <publisher topic="media.storage.catalog.image.resize"> + <connection name="db" exchange="magento-db" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaStorage/etc/queue_topology.xml b/app/code/Magento/MediaStorage/etc/queue_topology.xml new file mode 100644 index 0000000000000..9bb1ca5cef90e --- /dev/null +++ b/app/code/Magento/MediaStorage/etc/queue_topology.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="imageResizeBinding" topic="media.storage.catalog.image.resize" destinationType="queue" destination="media.storage.catalog.image.resize"/> + </exchange> +</config> diff --git a/app/code/Magento/Msrp/etc/di.xml b/app/code/Magento/Msrp/etc/di.xml index b8392b0bb0fe4..e617e153fd951 100644 --- a/app/code/Magento/Msrp/etc/di.xml +++ b/app/code/Magento/Msrp/etc/di.xml @@ -53,4 +53,14 @@ </argument> </arguments> </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="msrp" xsi:type="string">catalog_product</item> + <item name="msrp_display_actual_price_type" xsi:type="string">catalog_product</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Msrp/view/base/web/js/msrp.js b/app/code/Magento/Msrp/view/base/web/js/msrp.js index 2789491137bc1..65af87d85de51 100644 --- a/app/code/Magento/Msrp/view/base/web/js/msrp.js +++ b/app/code/Magento/Msrp/view/base/web/js/msrp.js @@ -352,8 +352,11 @@ define([ $(this.options.mapInfoLinks).show(); if (useDefaultPrice || !this.wasOpened) { - this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); - this.$popup.find(this.options.priceLabelId).html(options.realPrice); + if (this.$popup) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + } + $(this.options.displayPriceElement).html(msrpPrice); this.wasOpened = true; } diff --git a/app/code/Magento/Multishipping/Block/Checkout/Shipping.php b/app/code/Magento/Multishipping/Block/Checkout/Shipping.php index 77981c736b9e9..99450fc538070 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Shipping.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Shipping.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Block\Checkout; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Model\Quote\Address; +use Magento\Store\Model\ScopeInterface; /** * Mustishipping checkout shipping @@ -67,6 +70,8 @@ public function getCheckout() } /** + * Add page title and prepare layout + * * @return $this */ protected function _prepareLayout() @@ -78,6 +83,8 @@ protected function _prepareLayout() } /** + * Retrieves addresses + * * @return Address[] */ public function getAddresses() @@ -86,6 +93,8 @@ public function getAddresses() } /** + * Returns count of addresses + * * @return mixed */ public function getAddressCount() @@ -99,6 +108,8 @@ public function getAddressCount() } /** + * Retrieves the address items + * * @param Address $address * @return \Magento\Framework\DataObject[] */ @@ -106,7 +117,7 @@ public function getAddressItems($address) { $items = []; foreach ($address->getAllItems() as $item) { - if ($item->getParentItemId()) { + if ($item->getParentItemId() || !$item->getQuoteItemId()) { continue; } $item->setQuoteItem($this->getCheckout()->getQuote()->getItemById($item->getQuoteItemId())); @@ -118,6 +129,8 @@ public function getAddressItems($address) } /** + * Retrieves the address shipping method + * * @param Address $address * @return mixed */ @@ -127,6 +140,8 @@ public function getAddressShippingMethod($address) } /** + * Retrieves address shipping rates + * * @param Address $address * @return mixed */ @@ -137,22 +152,20 @@ public function getShippingRates($address) } /** + * Retrieves the carrier name by the code + * * @param string $carrierCode * @return string */ public function getCarrierName($carrierCode) { - if ($name = $this->_scopeConfig->getValue( - 'carriers/' . $carrierCode . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ) { - return $name; - } - return $carrierCode; + $name = $this->_scopeConfig->getValue('carriers/' . $carrierCode . '/title', ScopeInterface::SCOPE_STORE); + return $name ?: $carrierCode; } /** + * Retrieves the address edit url + * * @param Address $address * @return string */ @@ -162,6 +175,8 @@ public function getAddressEditUrl($address) } /** + * Retrieves the url for items edition + * * @return string */ public function getItemsEditUrl() @@ -170,6 +185,8 @@ public function getItemsEditUrl() } /** + * Retrieves the url for the post action + * * @return string */ public function getPostActionUrl() @@ -178,6 +195,8 @@ public function getPostActionUrl() } /** + * Retrieves the back url + * * @return string */ public function getBackUrl() @@ -186,6 +205,8 @@ public function getBackUrl() } /** + * Returns converted and formatted price + * * @param Address $address * @param float $price * @param bool $flag @@ -202,7 +223,7 @@ public function getShippingPrice($address, $price, $flag) } /** - * Retrieve text for items box + * Retrieves text for items box * * @param \Magento\Framework\DataObject $addressEntity * @return string diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php b/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php index c86caec733a17..38a30c1ee49e1 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php @@ -1,12 +1,19 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Multishipping\Controller\Checkout\Address; -class NewShipping extends \Magento\Multishipping\Controller\Checkout\Address +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Multishipping\Controller\Checkout\Address; + +/** + * Class NewShipping + * + * @package Address + */ +class NewShipping extends Address implements HttpGetActionInterface { /** * Create New Shipping address Form @@ -35,7 +42,7 @@ public function execute() if ($this->_getCheckout()->getCustomerDefaultShippingAddress()) { $addressForm->setBackUrl($this->_url->getUrl('*/checkout/addresses')); } else { - $addressForm->setBackUrl($this->_url->getUrl('*/cart/')); + $addressForm->setBackUrl($this->_url->getUrl('checkout/cart/')); } } $this->_view->renderLayout(); diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php b/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php deleted file mode 100644 index f88cdfc26fa9f..0000000000000 --- a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Multishipping\Controller\Checkout; - -/** - * Turns Off Multishipping mode for Quote. - */ -class Plugin -{ - /** - * @var \Magento\Checkout\Model\Cart - */ - protected $cart; - - /** - * @param \Magento\Checkout\Model\Cart $cart - */ - public function __construct(\Magento\Checkout\Model\Cart $cart) - { - $this->cart = $cart; - } - - /** - * Disable multishipping - * - * @param \Magento\Framework\App\Action\Action $subject - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeExecute(\Magento\Framework\App\Action\Action $subject) - { - $quote = $this->cart->getQuote(); - if ($quote->getIsMultiShipping()) { - $quote->setIsMultiShipping(0); - $this->cart->saveQuote(); - } - } -} diff --git a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php index 4ef36a7c8b6fc..b450232395b88 100644 --- a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php +++ b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php @@ -3,34 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Model\Cart\Controller; +use Magento\Checkout\Controller\Cart; +use Magento\Checkout\Model\Session; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; + +/** + * Cleans shipping addresses and item assignments after MultiShipping flow + */ class CartPlugin { /** - * @var \Magento\Quote\Api\CartRepositoryInterface + * @var CartRepositoryInterface */ private $cartRepository; /** - * @var \Magento\Checkout\Model\Session + * @var Session */ private $checkoutSession; /** - * @var \Magento\Customer\Api\AddressRepositoryInterface + * @var AddressRepositoryInterface */ private $addressRepository; /** - * @param \Magento\Quote\Api\CartRepositoryInterface $cartRepository - * @param \Magento\Checkout\Model\Session $checkoutSession - * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository + * @param CartRepositoryInterface $cartRepository + * @param Session $checkoutSession + * @param AddressRepositoryInterface $addressRepository */ public function __construct( - \Magento\Quote\Api\CartRepositoryInterface $cartRepository, - \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository + CartRepositoryInterface $cartRepository, + Session $checkoutSession, + AddressRepositoryInterface $addressRepository ) { $this->cartRepository = $cartRepository; $this->checkoutSession = $checkoutSession; @@ -38,20 +52,19 @@ public function __construct( } /** - * @param \Magento\Checkout\Controller\Cart $subject - * @param \Magento\Framework\App\RequestInterface $request + * Cleans shipping addresses and item assignments after MultiShipping flow + * + * @param Cart $subject + * @param RequestInterface $request * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws LocalizedException */ - public function beforeDispatch( - \Magento\Checkout\Controller\Cart $subject, - \Magento\Framework\App\RequestInterface $request - ) { - /** @var \Magento\Quote\Model\Quote $quote */ + public function beforeDispatch(Cart $subject, RequestInterface $request) + { + /** @var Quote $quote */ $quote = $this->checkoutSession->getQuote(); - - // Clear shipping addresses and item assignments after MultiShipping flow - if ($quote->isMultipleShippingAddresses()) { + if ($quote->isMultipleShippingAddresses() && $this->isCheckoutComplete()) { foreach ($quote->getAllShippingAddresses() as $address) { $quote->removeAddress($address->getId()); } @@ -59,12 +72,20 @@ public function beforeDispatch( $shippingAddress = $quote->getShippingAddress(); $defaultShipping = $quote->getCustomer()->getDefaultShipping(); if ($defaultShipping) { - $defaultCustomerAddress = $this->addressRepository->getById( - $defaultShipping - ); + $defaultCustomerAddress = $this->addressRepository->getById($defaultShipping); $shippingAddress->importCustomerAddressData($defaultCustomerAddress); } $this->cartRepository->save($quote); } } + + /** + * Checks whether the checkout flow is complete + * + * @return bool + */ + private function isCheckoutComplete() : bool + { + return (bool) ($this->checkoutSession->getStepData(State::STEP_SHIPPING)['is_complete'] ?? true); + } } diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 7105fd4e9d26d..7fa674505461e 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -625,6 +625,7 @@ public function setShippingMethods($methods) $addressId = $address->getId(); if (isset($methods[$addressId])) { $address->setShippingMethod($methods[$addressId]); + $address->setCollectShippingRates(true); } elseif (!$address->getShippingMethod()) { throw new \Magento\Framework\Exception\LocalizedException( __('Set shipping methods for all addresses. Verify the shipping methods and try again.') @@ -662,7 +663,9 @@ public function setPaymentMethod($payment) $quote->getPayment()->importData($payment); // shipping totals may be affected by payment method if (!$quote->isVirtual() && $quote->getShippingAddress()) { - $quote->getShippingAddress()->setCollectShippingRates(true); + foreach ($quote->getAllShippingAddresses() as $shippingAddress) { + $shippingAddress->setCollectShippingRates(true); + } $quote->setTotalsCollectedFlag(false)->collectTotals(); } $this->quoteRepository->save($quote); @@ -691,6 +694,19 @@ protected function _prepareOrder(\Magento\Quote\Model\Quote\Address $address) $this->quoteAddressToOrder->convert($address) ); + $shippingMethodCode = $address->getShippingMethod(); + if (isset($shippingMethodCode) && !empty($shippingMethodCode)) { + $rate = $address->getShippingRateByCode($shippingMethodCode); + $shippingPrice = $rate->getPrice(); + } else { + $shippingPrice = $order->getShippingAmount(); + } + $store = $order->getStore(); + $amountPrice = $store->getBaseCurrency() + ->convert($shippingPrice, $store->getCurrentCurrencyCode()); + $order->setBaseShippingAmount($shippingPrice); + $order->setShippingAmount($amountPrice); + $order->setQuote($quote); $order->setBillingAddress($this->quoteAddressToOrderAddress->convert($quote->getBillingAddress())); diff --git a/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php new file mode 100644 index 0000000000000..fff2346d76240 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Checkout\Model\Cart; +use Magento\Framework\App\Action\Action; + +/** + * Turns Off Multishipping mode for Quote. + */ +class DisableMultishippingMode +{ + /** + * @var Cart + */ + private $cart; + + /** + * @param Cart $cart + */ + public function __construct( + Cart $cart + ) { + $this->cart = $cart; + } + + /** + * Disable multishipping + * + * @param Action $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(Action $subject) + { + $quote = $this->cart->getQuote(); + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $extensionAttributes = $quote->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $extensionAttributes->setShippingAssignments([]); + } + $this->cart->saveQuote(); + } + } +} diff --git a/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php b/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php new file mode 100644 index 0000000000000..af19e4bc91f51 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Framework\Api\SearchResultsInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingProcessor; +use Magento\Quote\Model\ShippingAssignmentFactory; + +/** + * Plugin for multishipping quote processing. + */ +class MultishippingQuoteRepository +{ + /** + * @var ShippingAssignmentFactory + */ + private $shippingAssignmentFactory; + + /** + * @var ShippingProcessor + */ + private $shippingProcessor; + + /** + * @param ShippingAssignmentFactory $shippingAssignmentFactory + * @param ShippingProcessor $shippingProcessor + */ + public function __construct( + ShippingAssignmentFactory $shippingAssignmentFactory, + ShippingProcessor $shippingProcessor + ) { + $this->shippingAssignmentFactory = $shippingAssignmentFactory; + $this->shippingProcessor = $shippingProcessor; + } + + /** + * Process multishipping quote for get. + * + * @param CartRepositoryInterface $subject + * @param CartInterface $result + * @return CartInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGet( + CartRepositoryInterface $subject, + CartInterface $result + ) { + return $this->processQuote($result); + } + + /** + * Process multishipping quote for get list. + * + * @param CartRepositoryInterface $subject + * @param SearchResultsInterface $result + * + * @return SearchResultsInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetList( + CartRepositoryInterface $subject, + SearchResultsInterface $result + ) { + $items = []; + foreach ($result->getItems() as $item) { + $items[] = $this->processQuote($item); + } + $result->setItems($items); + + return $result; + } + + /** + * Remove shipping assignments for multishipping quote. + * + * @param CartRepositoryInterface $subject + * @param CartInterface $quote + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(CartRepositoryInterface $subject, CartInterface $quote) + { + $extensionAttributes = $quote->getExtensionAttributes(); + if ($quote->getIsMultiShipping() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $quote->getExtensionAttributes()->setShippingAssignments([]); + } + + return [$quote]; + } + + /** + * Set shipping assignments for multishipping quote according to customer selection. + * + * @param CartInterface $quote + * @return CartInterface + */ + private function processQuote(CartInterface $quote): CartInterface + { + if (!$quote->getIsMultiShipping() || !$quote instanceof Quote) { + return $quote; + } + + if ($quote->getExtensionAttributes() && $quote->getExtensionAttributes()->getShippingAssignments()) { + $shippingAssignments = []; + $addresses = $quote->getAllAddresses(); + + foreach ($addresses as $address) { + $quoteItems = $this->getQuoteItems($quote, $address); + if (!empty($quoteItems)) { + $shippingAssignment = $this->shippingAssignmentFactory->create(); + $shippingAssignment->setItems($quoteItems); + $shippingAssignment->setShipping($this->shippingProcessor->create($address)); + $shippingAssignments[] = $shippingAssignment; + } + } + + if (!empty($shippingAssignments)) { + $quote->getExtensionAttributes()->setShippingAssignments($shippingAssignments); + } + } + + return $quote; + } + + /** + * Returns quote items assigned to address. + * + * @param Quote $quote + * @param Quote\Address $address + * @return Quote\Item[] + */ + private function getQuoteItems(Quote $quote, Quote\Address $address): array + { + $quoteItems = []; + foreach ($address->getItemsCollection() as $addressItem) { + $quoteItem = $quote->getItemById($addressItem->getQuoteItemId()); + if ($quoteItem) { + $multishippingQuoteItem = clone $quoteItem; + $qty = $addressItem->getQty(); + $sku = $multishippingQuoteItem->getSku(); + if (isset($quoteItems[$sku])) { + $qty += $quoteItems[$sku]->getQty(); + } + $multishippingQuoteItem->setQty($qty); + $quoteItems[$sku] = $multishippingQuoteItem; + } + } + + return array_values($quoteItems); + } +} diff --git a/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php b/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php new file mode 100644 index 0000000000000..deac19e23a23a --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; + +/** + * Resets quote shipping assignments when item is removed from multishipping quote. + */ +class ResetShippingAssigment +{ + /** + * @var ShippingAssignmentProcessor + */ + private $shippingAssignmentProcessor; + + /** + * @param ShippingAssignmentProcessor $shippingAssignmentProcessor + */ + public function __construct( + ShippingAssignmentProcessor $shippingAssignmentProcessor + ) { + $this->shippingAssignmentProcessor = $shippingAssignmentProcessor; + } + + /** + * Resets quote shipping assignments when item is removed from multishipping quote. + * + * @param Quote $subject + * @param mixed $itemId + * + * @return array + */ + public function beforeRemoveItem(Quote $subject, $itemId): array + { + if ($subject->getIsMultiShipping()) { + $extensionAttributes = $subject->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $shippingAssignment = $this->shippingAssignmentProcessor->create($subject); + $extensionAttributes->setShippingAssignments([$shippingAssignment]); + } + } + + return [$itemId]; + } +} diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AdminSalesOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AdminSalesOrderActionGroup.xml new file mode 100644 index 0000000000000..dcd8bfd8d141b --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AdminSalesOrderActionGroup.xml @@ -0,0 +1,28 @@ +<?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/testSchema.xsd"> + <actionGroup name="AdminSalesOrderActionGroup"> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <grabTextFrom selector="{{AdminOrderTotalSection.subTotal}}" stepKey="grabValueForSubtotal"/> + <grabTextFrom selector="{{AdminOrderTotalSection.shippingAndHandling}}" stepKey="grabValueForShippingHandling"/> + <grabTextFrom selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="grabValueForGrandTotal"/> + <executeJS function=" + var grandTotal = '{$grabValueForGrandTotal}'.substr(1); + return (grandTotal);" stepKey="grandTotalValue"/> + <executeJS function=" + var subtotal = '{$grabValueForSubtotal}'.substr(1); + var handling = '{$grabValueForShippingHandling}'.substr(1); + var subtotalHandling = (parseFloat(subtotal) + parseFloat(handling)).toFixed(2); + return (subtotalHandling);" stepKey="sumTotalValue"/> + <assertEquals stepKey="assertSubTotalPrice"> + <expectedResult type="variable">$sumTotalValue</expectedResult> + <actualResult type="variable">$grandTotalValue</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontSalesOrderMatchesGrandTotalActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontSalesOrderMatchesGrandTotalActionGroup.xml new file mode 100644 index 0000000000000..559d759e0468d --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontSalesOrderMatchesGrandTotalActionGroup.xml @@ -0,0 +1,34 @@ +<?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/testSchema.xsd"> + <actionGroup name="AssertStorefrontSalesOrderMatchesGrandTotalActionGroup"> + <arguments> + <argument name="dataHref" type="string"/> + </arguments> + <!--Click on View Order Link--> + <click selector="{{StorefrontSalesOrderSection.viewOrderLink(dataHref)}}" stepKey="viewOrderAction"/> + <waitForPageLoad stepKey="waitForViewOrderPageToLoad"/> + <grabTextFrom selector="{{StorefrontSalesOrderSection.salesOrderPrice('subtotal')}}" stepKey="grabValueForSubtotal"/> + <grabTextFrom selector="{{StorefrontSalesOrderSection.salesOrderPrice('shipping')}}" stepKey="grabValueForShippingHandling"/> + <grabTextFrom selector="{{StorefrontSalesOrderSection.salesOrderPrice('grand_total')}}" stepKey="grabValueForGrandTotal"/> + <executeJS function=" + var grandTotal = '{$grabValueForGrandTotal}'.substr(1); + return (grandTotal);" stepKey="grandTotalValue"/> + <executeJS function=" + var subtotal = '{$grabValueForSubtotal}'.substr(1); + var handling = '{$grabValueForShippingHandling}'.substr(1); + var subtotalHandling = (parseFloat(subtotal) + parseFloat(handling)).toFixed(2); + return (subtotalHandling);" stepKey="sumTotalValue"/> + <assertEquals stepKey="assertSubTotalPrice"> + <expectedResult type="variable">$sumTotalValue</expectedResult> + <actualResult type="variable">$grandTotalValue</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml index f648c1026b539..35c42225d458b 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMinicartActionGroup.xml @@ -9,9 +9,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <actionGroup name="CheckingWithMinicartActionGroup"> - <waitForPageLoad stepKey="waitForCheckoutCartPageLoad"/> - <click stepKey="clickOnCollapsibleDiv" selector="{{MinicartSection.clickOnCollapsibleDiv}}"/> - <click stepKey="clickOnShippingMethodRadioButton" selector="{{MinicartSection.shippingMethodRadioButton}}"/> + <click selector="{{MinicartSection.clickOnCollapsibleDiv}}" stepKey="clickOnCollapsibleDiv"/> + <click selector="{{MinicartSection.shippingMethodRadioButton}}" stepKey="clickOnShippingMethodRadioButton"/> <waitForPageLoad stepKey="waitForShippingPriceToBeChanged"/> <grabTextFrom selector="{{MinicartSection.shippingMethodRadioText}}" stepKey="shippingMethodRadioText"/> <grabTextFrom selector="{{MinicartSection.shippingMethodSubtotalPrice}}" stepKey="shippingMethodSubtotalPrice"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml index 333c2aec6c28e..34ce38bd0d935 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml @@ -9,19 +9,40 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <actionGroup name="CheckingWithSingleAddressActionGroup"> - <click stepKey="clickOnCheckoutWithMultipleAddresses" selector="{{SingleShippingSection.checkoutWithMultipleAddresses}}"/> + <click selector="{{SingleShippingSection.checkoutWithMultipleAddresses}}" stepKey="clickOnCheckoutWithMultipleAddresses"/> <waitForPageLoad stepKey="waitForMultipleAddressPageLoad"/> - <click stepKey="goToShippingInformation" selector="{{SingleShippingSection.goToShippingInfo}}"/> + <click selector="{{SingleShippingSection.goToShippingInfo}}" stepKey="goToShippingInformation"/> <waitForPageLoad stepKey="waitForShippingPageLoad"/> </actionGroup> <actionGroup name="CheckingWithMultipleAddressesActionGroup" extends="CheckingWithSingleAddressActionGroup"> - <grabTextFrom stepKey="firstShippingAddressValue" selector="{{MultishippingSection.firstShippingAddressValue}}" after="waitForMultipleAddressPageLoad" /> - <selectOption selector="{{MultishippingSection.firstShippingAddressOption}}" userInput="{$firstShippingAddressValue}" stepKey="selectFirstShippingMethod" after="firstShippingAddressValue" /> - <waitForPageLoad stepKey="waitForSecondShippingAddresses" after="selectFirstShippingMethod" /> - <grabTextFrom stepKey="secondShippingAddressValue" selector="{{MultishippingSection.secondShippingAddressValue}}" after="waitForSecondShippingAddresses" /> - <selectOption selector="{{MultishippingSection.secondShippingAddressOption}}" userInput="{$secondShippingAddressValue}" stepKey="selectSecondShippingMethod" after="secondShippingAddressValue" /> - <click stepKey="clickOnUpdateAddress" selector="{{SingleShippingSection.updateAddress}}" after="selectSecondShippingMethod" /> - <waitForPageLoad stepKey="waitForShippingInformation" after="clickOnUpdateAddress" /> + <arguments> + <argument name="addressOption1" type="string" defaultValue="1"/> + <argument name="addressOption2" type="string" defaultValue="2"/> + </arguments> + <grabTextFrom selector="{{MultishippingSection.shippingAddressOptions(addressOption1,addressOption1)}}" after="waitForMultipleAddressPageLoad" stepKey="firstShippingAddressValue"/> + <selectOption selector="{{MultishippingSection.shippingAddressSelector(addressOption1)}}" userInput="{$firstShippingAddressValue}" after="firstShippingAddressValue" stepKey="selectFirstShippingMethod"/> + <waitForPageLoad after="selectFirstShippingMethod" stepKey="waitForSecondShippingAddresses"/> + <grabTextFrom selector="{{MultishippingSection.shippingAddressOptions(addressOption2,addressOption2)}}" after="waitForSecondShippingAddresses" stepKey="secondShippingAddressValue"/> + <selectOption selector="{{MultishippingSection.shippingAddressSelector(addressOption2)}}" userInput="{$secondShippingAddressValue}" after="secondShippingAddressValue" stepKey="selectSecondShippingMethod"/> + <click selector="{{SingleShippingSection.updateAddress}}" after="selectSecondShippingMethod" stepKey="clickOnUpdateAddress"/> + <waitForPageLoad after="clickOnUpdateAddress" stepKey="waitForShippingInformation"/> + </actionGroup> + <actionGroup name="StorefrontCheckoutWithMultipleAddressesActionGroup"> + <click selector="{{SingleShippingSection.checkoutWithMultipleAddresses}}" stepKey="clickOnCheckoutWithMultipleAddresses"/> + <waitForPageLoad stepKey="waitForMultipleAddressPageLoad"/> + </actionGroup> + <actionGroup name="StorefrontSelectAddressActionGroup"> + <arguments> + <argument name="sequenceNumber" type="string" defaultValue="1"/> + <argument name="option" type="string" defaultValue="1"/> + </arguments> + <selectOption selector="{{MultishippingSection.selectShippingAddress(sequenceNumber)}}" userInput="{{option}}" stepKey="selectShippingAddress"/> + </actionGroup> + <actionGroup name="StorefrontSaveAddressActionGroup"> + <click selector="{{SingleShippingSection.updateAddress}}" stepKey="clickOnUpdateAddress"/> + <waitForPageLoad time="90" stepKey="waitForShippingInformationAfterUpdated"/> + <click selector="{{SingleShippingSection.goToShippingInfo}}" stepKey="goToShippingInformation"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml index efb860e314780..871c71de522c7 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml @@ -9,12 +9,18 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <actionGroup name="PlaceOrderActionGroup"> - <waitForPageLoad stepKey="waitForPlaceOrderPageLoad"/> - <!-- place order and check the order number--> - <click stepKey="checkoutMultishipmentPlaceOrder" selector="{{SingleShippingSection.placeOrder}}" /> + <click selector="{{SingleShippingSection.placeOrder}}" stepKey="checkoutMultiShipmentPlaceOrder"/> <waitForPageLoad stepKey="waitForSuccessfullyPlacedOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> - + </actionGroup> + <actionGroup name="StorefrontPlaceOrderForMultipleAddressesActionGroup" extends="PlaceOrderActionGroup"> + <arguments> + <argument name="firstOrderPosition" type="string" defaultValue="1"/> + <argument name="secondOrderPosition" type="string" defaultValue="2"/> + </arguments> + <grabTextFrom selector="{{StorefrontSalesOrderSection.orderLinkByPosition(firstOrderPosition)}}" after="waitForLoadSuccessPage" stepKey="getFirstOrderId"/> + <grabAttributeFrom selector="{{StorefrontSalesOrderSection.orderLinkByPosition(firstOrderPosition)}}" userInput="href" after="getFirstOrderId" stepKey="dataHrefForFirstOrder"/> + <grabTextFrom selector="{{StorefrontSalesOrderSection.orderLinkByPosition(secondOrderPosition)}}" after="dataHrefForFirstOrder" stepKey="getSecondOrderId"/> + <grabAttributeFrom selector="{{StorefrontSalesOrderSection.orderLinkByPosition(secondOrderPosition)}}" userInput="href" after="getSecondOrderId" stepKey="dataHrefForSecondOrder"/> </actionGroup> </actionGroups> - diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml index af7d897910ca3..638c5dd8dde61 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml @@ -9,31 +9,37 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <actionGroup name="ReviewOrderForSingleShipmentActionGroup"> - <waitForPageLoad stepKey="waitForReviewOrderPageLoad"/> - <grabTextFrom selector="{{ReviewOrderSection.shippingMethodBasePrice}}" stepKey="shippingMethodBasePrice"/> - <grabTextFrom selector="{{ReviewOrderSection.shippingMethodSubtotalPrice}}" stepKey="shippingMethodSubtotalPrice"/> + <arguments> + <argument name="totalName" type="string" defaultValue="Shipping & Handling"/> + <argument name="totalPosition" type="string" defaultValue="1"/> + </arguments> + <grabTextFrom selector="{{ReviewOrderSection.shippingMethodBasePrice(totalPosition)}}" stepKey="shippingMethodBasePrice"/> + <grabTextFrom selector="{{ReviewOrderSection.shippingMethodSubtotalPrice(totalPosition,totalName)}}" stepKey="shippingMethodSubtotalPrice"/> <assertEquals stepKey="assertShippingMethodPrice"> <expectedResult type="string">$shippingMethodSubtotalPrice</expectedResult> <actualResult type="string">$shippingMethodBasePrice</actualResult> </assertEquals> </actionGroup> <actionGroup name="ReviewOrderForMultiShipmentActionGroup"> - <waitForPageLoad stepKey="waitForFirstShippingMethod" /> + <arguments> + <argument name="totalNameForFirstOrder" type="string" defaultValue="Shipping & Handling"/> + <argument name="totalPositionForFirstOrder" type="string" defaultValue="1"/> + <argument name="totalNameForSecondOrder" type="string" defaultValue="Shipping & Handling"/> + <argument name="totalPositionForSecondOrder" type="string" defaultValue="2"/> + </arguments> <!--Check First Shipping Method Price--> - <grabTextFrom selector="{{ReviewOrderSection.firstShippingMethodBasePrice}}" stepKey="firstShippingMethodBasePrice"/> - <grabTextFrom selector="{{ReviewOrderSection.firstShippingMethodSubtotalPrice}}" stepKey="firstShippingMethodSubtotalPrice"/> + <grabTextFrom selector="{{ReviewOrderSection.shippingMethodBasePrice(totalPositionForFirstOrder)}}" stepKey="firstShippingMethodBasePrice"/> + <grabTextFrom selector="{{ReviewOrderSection.shippingMethodSubtotalPrice(totalPositionForFirstOrder,totalNameForFirstOrder)}}" stepKey="firstShippingMethodSubtotalPrice"/> <assertEquals stepKey="assertShippingMethodPrice"> <expectedResult type="string">$firstShippingMethodSubtotalPrice</expectedResult> <actualResult type="string">$firstShippingMethodBasePrice</actualResult> </assertEquals> <!--Check Second Shipping Method Price--> - <grabTextFrom selector="{{ReviewOrderSection.secondShippingMethodBasePrice}}" stepKey="secondShippingMethodBasePrice" /> - <grabTextFrom selector="{{ReviewOrderSection.secondShippingMethodSubtotalPrice}}" stepKey="secondShippingMethodSubtotalPrice" /> + <grabTextFrom selector="{{ReviewOrderSection.shippingMethodBasePrice(totalPositionForSecondOrder)}}" stepKey="secondShippingMethodBasePrice"/> + <grabTextFrom selector="{{ReviewOrderSection.shippingMethodSubtotalPrice(totalPositionForSecondOrder,totalNameForSecondOrder)}}" stepKey="secondShippingMethodSubtotalPrice"/> <assertEquals stepKey="assertSecondShippingMethodPrice" > <expectedResult type="string">$secondShippingMethodSubtotalPrice</expectedResult> <actualResult type="string">$secondShippingMethodBasePrice</actualResult> </assertEquals> - </actionGroup> </actionGroups> - diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml index 3f7578953df70..63fbaea72cc50 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml @@ -10,7 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <actionGroup name="SelectBillingInfoActionGroup"> <waitForPageLoad stepKey="waitForBillingInfoPageLoad"/> - <click stepKey="goToReviewOrder" selector="{{PaymentMethodSection.goToReviewOrder}}"/> + <click selector="{{PaymentMethodSection.goToReviewOrder}}" stepKey="goToReviewOrder"/> </actionGroup> </actionGroups> - diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml index af0b2467862ba..a5aac6b32011b 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml @@ -12,22 +12,26 @@ <arguments> <argument name="shippingMethodType" type="string" defaultValue="Fixed"/> </arguments> - <waitForPageLoad stepKey="waitForShippingInfoPageLoad"/> <selectOption selector="{{ShippingMethodSection.shippingMethodRadioButton}}" userInput="{{shippingMethodType}}" stepKey="selectShippingMethod"/> <waitForPageLoad stepKey="waitForRadioOptions"/> - <click stepKey="goToBillingInformation" selector="{{ShippingMethodSection.goToBillingInfo}}"/> + <click selector="{{ShippingMethodSection.goToBillingInfo}}" stepKey="goToBillingInformation"/> </actionGroup> <actionGroup name="SelectMultiShippingInfoActionGroup"> <arguments> + <argument name="shippingMethodPosition1" type="string" defaultValue="1"/> + <argument name="shippingMethodPosition2" type="string" defaultValue="2"/> <argument name="shippingMethodType1" type="string" defaultValue="Fixed"/> <argument name="shippingMethodType2" type="string" defaultValue="Free"/> </arguments> - <waitForPageLoad stepKey="waitForShippingInfoPageLoad"/> - <selectOption selector="{{ShippingMethodSection.firstShippingMethodRadioButton}}" userInput="{{shippingMethodType1}}" stepKey="selectShippingMethod1"/> + <selectOption selector="{{ShippingMethodSection.selectShippingMethod(shippingMethodPosition1,shippingMethodPosition1)}}" userInput="{{shippingMethodType1}}" stepKey="selectShippingMethod1"/> <waitForPageLoad stepKey="waitForSecondShippingMethod"/> - <selectOption selector="{{ShippingMethodSection.secondShippingMethodRadioButton}}" userInput="{{shippingMethodType2}}" stepKey="selectShippingMethod2"/> + <selectOption selector="{{ShippingMethodSection.selectShippingMethod(shippingMethodPosition2,shippingMethodPosition2)}}" userInput="{{shippingMethodType2}}" stepKey="selectShippingMethod2"/> <waitForPageLoad stepKey="waitForRadioOptions"/> - <click stepKey="goToBillingInformation" selector="{{ShippingMethodSection.goToBillingInfo}}"/> + <click selector="{{ShippingMethodSection.goToBillingInfo}}" stepKey="goToBillingInformation"/> + </actionGroup> + <actionGroup name="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup"> + <waitForPageLoad stepKey="waitForShippingInfo"/> + <click selector="{{ShippingMethodSection.goToBillingInfo}}" stepKey="goToBillingInformation"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontMultishippingCheckoutActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontMultishippingCheckoutActionGroup.xml new file mode 100644 index 0000000000000..c5dee010239d7 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontMultishippingCheckoutActionGroup.xml @@ -0,0 +1,28 @@ +<?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/testSchema.xsd"> + <actionGroup name="StorefrontCheckoutShippingSelectMultipleAddressesActionGroup"> + <arguments> + <argument name="firstAddress" type="string" defaultValue="{{CustomerAddressSimple.street[0]}}"/> + <argument name="secondAddress" type="string" defaultValue="{{CustomerAddressSimple.street[1]}}"/> + </arguments> + <selectOption selector="{{StorefrontCheckoutShippingMultipleAddressesSection.selectedMultipleShippingAddress('1')}}" userInput="{{firstAddress}}" stepKey="selectShippingAddressForTheFirstItem"/> + <selectOption selector="{{StorefrontCheckoutShippingMultipleAddressesSection.selectedMultipleShippingAddress('2')}}" userInput="{{secondAddress}}" stepKey="selectShippingAddressForTheSecondItem"/> + <click selector="{{CheckoutSuccessMainSection.continueShoppingButton}}" stepKey="clickToGoToInformationButton"/> + </actionGroup> + <actionGroup name="StorefrontGoCheckoutWithMultipleAddresses"> + <click selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="clickToMultipleAddressShippingButton"/> + </actionGroup> + <actionGroup name="StorefrontGoToBillingInformationActionGroup"> + <click selector="{{StorefrontMultipleShippingMethodSection.continueToBillingInformationButton}}" stepKey="clickToContinueToBillingInformationButton"/> + <waitForPageLoad stepKey="waitForBillingPage"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Data/MultishippingSalesRuleData.xml b/app/code/Magento/Multishipping/Test/Mftf/Data/MultishippingSalesRuleData.xml new file mode 100644 index 0000000000000..7c79081245fd6 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Data/MultishippingSalesRuleData.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="CartPriceRuleConditionForSubtotalForMultiShipping" extends="CartPriceRuleConditionAppliedForSubtotal"> + <data key="apply">Percent of product price discount</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="subtotal">50</data> + <data key="apply_to_shipping">1</data> + <data key="simple_free_shipping">For matching items only</data> + <data key="condition1">Subtotal</data> + <data key="condition2">Shipping Method</data> + <data key="rule1">equals or greater than</data> + <data key="shippingMethod">[flatrate] Fixed</data> + <data key="ruleToChange1">is</data> + </entity> +</entities> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml b/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml index 001002e98271c..beee76a632b22 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml @@ -15,4 +15,4 @@ <section name="PaymentMethodSection"/> <section name="ReviewOrderSection"/> </page> -</pages> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml index 45fafc3105c38..cd408f5600e3d 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml @@ -16,9 +16,15 @@ </section> <section name="MultishippingSection"> <element name="checkoutWithMultipleAddresses" type="button" selector="//span[text()='Check Out with Multiple Addresses']"/> - <element name="firstShippingAddressValue" type="select" selector="//table//tbody//tr[position()=1]//td[position()=3]//div//select//option[2]"/> - <element name="firstShippingAddressOption" type="select" selector="//table//tbody//tr[position()=1]//td[position()=3]//div//select"/> - <element name="secondShippingAddressValue" type="select" selector="//table//tbody//tr[position()=2]//td[position()=3]//div//select//option[1]"/> - <element name="secondShippingAddressOption" type="select" selector="//table//tbody//tr[position()=2]//td[position()=3]//div//select"/> + <element name="shippingMultipleCheckout" type="button" selector=".action.multicheckout"/> + <element name="shippingAddressSelector" type="select" selector="//tr[position()={{addressPosition}}]//td[@data-th='Send To']//select" parameterized="true"/> + <element name="shippingAddressOptions" type="select" selector="#multiship-addresses-table tbody tr:nth-of-type({{addressPosition}}) .col.address select option:nth-of-type({{optionIndex}})" parameterized="true"/> + <element name="selectShippingAddress" type="select" selector="(//table[@id='multiship-addresses-table'] //div[@class='field address'] //select)[{{sequenceNumber}}]" parameterized="true"/> + </section> + <section name="StorefrontMultipleShippingMethodSection"> + <element name="orderId" type="text" selector=".shipping-list:nth-child({{rowNum}}) .order-id" parameterized="true"/> + <element name="goToReviewYourOrderButton" type="button" selector="#payment-continue"/> + <element name="continueToBillingInformationButton" type="button" selector=".action.primary.continue"/> + <element name="successMessage" type="text" selector=".multicheckout.success"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml index 4e7f4a497ad4d..2d47b54d84b9c 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml @@ -9,6 +9,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <section name="PaymentMethodSection"> - <element name="goToReviewOrder" type="button" selector="//span[text()='Go to Review Your Order']"/> + <element name="goToReviewOrder" type="button" selector="#payment-continue"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml index e13f28929dcc8..de33c28bfb1f2 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml @@ -9,19 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <section name="ReviewOrderSection"> - <element name="shippingMethodBasePrice" type="text" selector="//div[@class='block block-shipping'][position()=1]//div[@class='block-content'][position()=1]//div[@class='box box-shipping-method']//div[@class='box-content']//span[@class='price']"/> - <element name="shippingMethodSubtotalPrice" type="text" selector="//div[@class='block-content'][position()=1]//table[position()=1]//tr[position()=2]//td[@class='amount']//span[@class='price']"/> - <element name="firstShippingMethodBasePrice" type="text" selector="//div[@class='block block-shipping'][position()=1]//div[@class='block-content'][position()=1]//div[@class='box box-shipping-method']//div[@class='box-content']//span[@class='price']"/> - <element name="secondShippingMethodBasePrice" type="text" selector="//div[@class='block block-shipping'][position()=1]//div[@class='block-content'][position()=2]//div[@class='box box-shipping-method']//div[@class='box-content']//span[@class='price']"/> - <element name="firstShippingMethodSubtotalPrice" type="text" selector="//div[@class='block-content'][position()=1]//table[position()=1]//tr[position()=2]//td//span[@class='price']"/> - <element name="secondShippingMethodSubtotalPrice" type="text" selector="//div[@class='block-content'][position()=2]//table[position()=1]//tr[position()=2]//td//span[@class='price']"/> - <element name="firstOrderSubtotalPrice" type="text" selector="//div[@class='block-content'][position()=1]//table[position()=1]//tr[@class='totals sub'][position()=1]//td[@data-th='Subtotal']//span[@class='price']"/> - <element name="secondOrderSubtotalPrice" type="text" selector="//div[@class='block-content'][position()=2]//table[position()=1]//tr[@class='totals sub'][position()=1]//td[@data-th='Subtotal']//span[@class='price']"/> - <element name="firstOrderTaxPrice" type="text" selector="//div[@class='block-content'][position()=1]//table[position()=1]//tr[@class='totals-tax'][position()=1]//td[@data-th='Tax']//span[@class='price']"/> - <element name="secondOrderTaxPrice" type="text" selector="//div[@class='block-content'][position()=2]//table[position()=1]//tr[@class='totals-tax'][position()=1]//td[@data-th='Tax']//span[@class='price']"/> - <element name="firstOrderTotalPrice" type="text" selector="//div[@class='block-content'][position()=1]//table[position()=1]//tr[@class='grand totals'][position()=1]//td//span[@class='price']"/> - <element name="secondOrderTotalPrice" type="text" selector="//div[@class='block-content'][position()=2]//table[position()=1]//tr[@class='grand totals'][position()=1]//td//span[@class='price']"/> - <element name="grandTotalPrice" type="text" selector="//div[@class='checkout-review']//div[@class='grand totals']//span[@class='price']"/> + <element name="shippingMethodBasePrice" type="text" selector="//div[@class='block-content'][position()={{shippingMethodPosition}}]//div[@class='box box-shipping-method'][position()=1]//span[@class='price']" parameterized="true"/> + <element name="shippingMethodSubtotalPrice" type="text" selector="//div[@class='block-content'][position()={{shippingMethodPosition}}]//td[@class='amount'][contains(@data-th,'{{priceType}}')]//span[@class='price']" parameterized="true"/> </section> </sections> - diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml index 6a2290bcf1a43..c4dd2494f7fe8 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml @@ -10,9 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <section name="ShippingMethodSection"> <element name="shippingMethodRadioButton" type="select" selector="//input[@class='radio']"/> - <element name="firstShippingMethodRadioButton" type="select" selector="//div[@class='block block-shipping'][position()=1]//div[@class='block-content']//div[@class='box box-shipping-method']//div[@class='box-content']//dl//dd[position()=1]//fieldset//div//div//input[@class='radio']"/> - <element name="secondShippingMethodRadioButton" type="select" selector="//div[@class='block block-shipping'][position()=2]//div[@class='block-content']//div[@class='box box-shipping-method']//div[@class='box-content']//dl//dd[position()=2]//fieldset//div//div//input[@class='radio']"/> - <element name="goToBillingInfo" type="button" selector="//span[text()='Continue to Billing Information']"/> + <element name="selectShippingMethod" type="radio" selector="//div[@class='block block-shipping'][position()={{shippingBlockPosition}}]//dd[position()={{shippingMethodPosition}}]//input[@class='radio']" parameterized="true" timeout="5"/> + <element name="goToBillingInfo" type="button" selector=".action.primary.continue"/> </section> </sections> - diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutShippingMultipleAddressesSection.xml similarity index 59% rename from app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml rename to app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutShippingMultipleAddressesSection.xml index e54f9808fd49e..34427bda9334a 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutShippingMultipleAddressesSection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StoresSubmenuSection"> - <element name="configuration" type="button" selector="#menu-magento-backend-stores li[data-ui-id='menu-magento-config-system-config']"/> + <section name="StorefrontCheckoutShippingMultipleAddressesSection"> + <element name="selectedMultipleShippingAddress" type="select" selector=".table tr:nth-of-type({{selectNumber}}) select" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontSalesOrderSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontSalesOrderSection.xml new file mode 100644 index 0000000000000..94546dcfef9a0 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontSalesOrderSection.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:Test/etc/testSchema.xsd"> + <section name="StorefrontSalesOrderSection"> + <element name="orderLinkByPosition" type="text" selector="//li[@class='shipping-list'][position()={{orderLinkPosition}}]//a" parameterized="true"/> + <element name="viewOrderLink" type="text" selector="//td[@data-th='Actions']//a[contains(@href,'{{orderLink}}')]//span[text()='View Order']" parameterized="true" timeout="5"/> + <element name="salesOrderPrice" type="text" selector="//div[@class='order-details-items ordered']//tr[@class='{{priceType}}']//td[@class='amount']//span[@class='price']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml index 271f6e707cd69..3a58ead3b6dfa 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml @@ -57,6 +57,7 @@ <deleteData stepKey="deleteProduct1" createDataKey="product1"/> <deleteData stepKey="deleteProduct2" createDataKey="product2"/> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> <actionGroup ref="logout" stepKey="logout"/> </after> </test> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml index f425a44130ecb..c9f1856249762 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml @@ -57,6 +57,7 @@ <deleteData stepKey="deleteProduct1" createDataKey="product1"/> <deleteData stepKey="deleteProduct2" createDataKey="product2"/> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> <actionGroup ref="logout" stepKey="logout"/> </after> </test> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml index 8d5a58acc7e18..c2fd978cf3137 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml @@ -37,6 +37,16 @@ </actionGroup> </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData stepKey="deleteCategory" createDataKey="category"/> + <deleteData stepKey="deleteProduct1" createDataKey="product1"/> + <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <actionGroup ref="logout" stepKey="logoutAdmin"/> + </after> + <amOnPage url="$$product1.name$$.html" stepKey="goToProduct1"/> <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProduct1"> <argument name="productName" value="$$product1.name$$"/> @@ -52,14 +62,7 @@ <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"/> <amOnPage url="/checkout/cart/index/" stepKey="amOnCheckoutCartIndexPage"/> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCartAgain"/> + <waitForPageLoad stepKey="waitForMinicartPageLoad"/> <actionGroup ref="CheckingWithMinicartActionGroup" stepKey="checkoutWithMinicart"/> - - <after> - <deleteData stepKey="deleteCategory" createDataKey="category"/> - <deleteData stepKey="deleteProduct1" createDataKey="product1"/> - <deleteData stepKey="deleteProduct2" createDataKey="product2"/> - <deleteData stepKey="deleteCustomer" createDataKey="customer"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> </test> </tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMyAccountWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMyAccountWithMultishipmentTest.xml new file mode 100644 index 0000000000000..eec8a40877bb3 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMyAccountWithMultishipmentTest.xml @@ -0,0 +1,117 @@ +<?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="StorefrontMyAccountWithMultishipmentTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Shipping price shows 0 on Order view page after multiple address checkout"/> + <title value="Verify Shipping price for Storefront after multiple address checkout"/> + <description value="Verify that shipping price on My account matches with shipping method prices after multiple addresses checkout (Order view page)"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-19303"/> + <group value="multishipping"/> + <skip> + <issueId value="MC-22683"/> + </skip> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product1"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="SimpleProduct" stepKey="product2"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> + <!-- Set configurations --> + <magentoCLI command="config:set {{EnableMultiShippingCheckoutMultiple.path}} {{EnableMultiShippingCheckoutMultiple.value}}" stepKey="allowShippingToMultipleAddresses"/> + <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> + <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> + <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + </before> + <after> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="customerLogout"/> + <magentoCLI command="config:set {{DisableMultiShippingCheckoutMultiple.path}} {{DisableMultiShippingCheckoutMultiple.value}}" stepKey="withdrawShippingToMultipleAddresses"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllFilters"/> + <actionGroup ref="logout" stepKey="logoutAdmin"/> + </after> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProduct1ToCart"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProduct2ToCart"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <actionGroup ref="CheckingWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <waitForPageLoad stepKey="waitForShippingInfoPageLoad"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShipping"/> + <!--Select Check / Money order Payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="checkoutWithPaymentMethod"/> + <waitForPageLoad stepKey="waitForReviewOrderPageLoad"/> + <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"> + <argument name="totalNameForFirstOrder" value="Shipping & Handling"/> + <argument name="totalPositionForFirstOrder" value="1"/> + <argument name="totalNameForSecondOrder" value="Shipping & Handling"/> + <argument name="totalPositionForSecondOrder" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPlaceOrderPageLoad"/> + <actionGroup ref="StorefrontPlaceOrderForMultipleAddressesActionGroup" stepKey="placeOrder"> + <argument name="firstOrderPosition" value="1"/> + <argument name="secondOrderPosition" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToSalesOrder"/> + <actionGroup ref="AssertStorefrontSalesOrderMatchesGrandTotalActionGroup" stepKey="checkSalesOrderForFirstOrder"> + <argument name="dataHref" value="$dataHrefForFirstOrderPlaceOrder"/> + </actionGroup> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToSalesOrder2"/> + <waitForPageLoad stepKey="waitForOrderPageLoad2"/> + <actionGroup ref="AssertStorefrontSalesOrderMatchesGrandTotalActionGroup" stepKey="checkSalesOrderForSecondOrder"> + <argument name="dataHref" value="$dataHrefForSecondOrderPlaceOrder"/> + </actionGroup> + <waitForPageLoad stepKey="waitForAdminPageToLoad"/> + <!-- Go to Stores > Configuration > Sales > Orders --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onAdminOrdersPage"/> + <waitForPageLoad stepKey="waitForOrderPageLoad3"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <!--Assert order in orders grid --> + <!-- Go to order page --> + <actionGroup ref="OpenOrderById" stepKey="openFirstOrderPage"> + <argument name="orderId" value="{$getFirstOrderIdPlaceOrder}"/> + </actionGroup> + <!-- Check status --> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeFirstOrderPendingStatus"/> + <actionGroup ref="AdminSalesOrderActionGroup" stepKey="validateOrderTotalsForFirstOrder"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onAdminOrdersPage2"/> + <waitForPageLoad stepKey="waitForOrderPageLoad4"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters2"/> + <!-- Go to order page --> + <actionGroup ref="OpenOrderById" stepKey="openSecondOrderPage"> + <argument name="orderId" value="{$getSecondOrderIdPlaceOrder}"/> + </actionGroup> + <!-- Check status --> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeSecondOrderPendingStatus"/> + <actionGroup ref="AdminSalesOrderActionGroup" stepKey="validateOrderTotalsForSecondOrder"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="gotToHomePage"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml new file mode 100644 index 0000000000000..02187658a8781 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml @@ -0,0 +1,35 @@ +<?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="StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest" extends="StoreFrontCheckingWithMultishipmentTest"> + <annotations> + <features value="Multi shipment and Cart Price Rule"/> + <stories value="Checking cart price rule for multi shipment with multiple shipment addresses on front end order page"/> + <title value="Checking sub total amount and free shipping is applied with multiple shipment addresses on front end order page"/> + <description value="Cart Price Rules not working and free shipping not applied for Multi shipping "/> + <severity value="MAJOR"/> + <testCaseId value="MC-21738"/> + <group value="Multishipment"/> + <group value="SalesRule"/> + </annotations> + <before> + <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> + </before> + <after> + <magentoCLI command="config:set multishipping/options/checkout_multiple 0" stepKey="disableShippingToMultipleAddresses"/> + </after> + <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalActionGroup" before="goToProduct1" stepKey="createSubtotalCartPriceRuleActionsSection"> + <argument name="ruleName" value="CartPriceRuleConditionForSubtotalForMultiShipping"/> + </actionGroup> + <actionGroup ref="DeleteCartPriceRuleByName" after="placeOrder" stepKey="deleteCreatedCartPriceRule"> + <argument name="ruleName" value="{$getSubtotalRuleCreateSubtotalCartPriceRuleActionsSection}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml new file mode 100644 index 0000000000000..138ab5df26ab0 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml @@ -0,0 +1,121 @@ +<?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="StorefrontCheckoutWithMultipleAddressesTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Multiple Shipping"/> + <title value="Place an order with three different addresses"/> + <description value="Place an order with three different addresses"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17461"/> + <useCaseId value="MAGETWO-99490"/> + <group value="Multishipment"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Set configurations --> + <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> + <!-- Create simple products --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="secondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Customer_US_UK_DE" stepKey="createCustomerWithMultipleAddresses"/> + </before> + <after> + <!-- Delete created data --> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> + </after> + <!-- Login to the Storefront as created customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomerWithMultipleAddresses$$"/> + </actionGroup> + <!-- Open the first product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToFirstProductPage"> + <argument name="productUrl" value="$$firstProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add the first product to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addFirstProductToCart"> + <argument name="productName" value="$$firstProduct.name$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <!-- Open the second product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToSecondProductPage"> + <argument name="productUrl" value="$$secondProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add the second product to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addSecondProductToCart"> + <argument name="productName" value="$$secondProduct.name$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <!--Go to Cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <!--Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFirstAddress"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSecondAddress"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <!-- Click 'Continue to Billing Information' --> + <actionGroup ref="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup" stepKey="useDefaultShippingMethod"/> + <!-- Click 'Go to Review Your Order' --> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="useDefaultBillingMethod"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <!-- Open the first product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToFirstProductPageSecondTime"> + <argument name="productUrl" value="$$firstProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add three identical products to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addIdenticalProductsToCart"> + <argument name="productName" value="$$firstProduct.name$$"/> + <argument name="productQty" value="3"/> + </actionGroup> + <!--Go to Cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCartWithIdenticalProducts"/> + <!--Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithThreeDifferentAddresses"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFirstAddressFromThree"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSecondAddressFromThree"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectThirdAddressFromThree"> + <argument name="sequenceNumber" value="3"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveThreeDifferentAddresses"/> + <!-- Click 'Continue to Billing Information' --> + <actionGroup ref="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup" stepKey="useDefaultShippingMethodForIdenticalProducts"/> + <!-- Click 'Go to Review Your Order' --> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="UseDefaultBillingMethodForIdenticalProducts"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrderWithIdenticalProducts"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml new file mode 100644 index 0000000000000..fd79d4d954cd4 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -0,0 +1,117 @@ +<?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="StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Multishipping"/> + <title value="Process multishipping checkout when Cart page is opened in another tab"/> + <description value="Process multishipping checkout when Cart page is opened in another tab"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17871"/> + <useCaseId value="MC-17469"/> + <group value="multishipping"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Set configurations --> + <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> + <!-- Create two simple products --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomerWithMultipleAddresses"/> + </before> + <after> + <!-- Delete created data --> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> + </after> + <!-- Login to the Storefront as created customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomerWithMultipleAddresses$$"/> + </actionGroup> + <!-- Add two products to the Shopping Cart --> + <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.name$$)}}" stepKey="amOnStorefrontProductFirstPage"/> + <waitForPageLoad stepKey="waitForTheFirstProduct"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddProductToCart"> + <argument name="product" value="$$createFirstProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSecondProduct.name$$)}}" stepKey="amOnStorefrontSecondProductPage"/> + <waitForPageLoad stepKey="waitForPageLoadForTheSecondProduct"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSecondProductToCart"> + <argument name="product" value="$$createSecondProduct$$"/> + <argument name="productCount" value="2"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnShoppingCartPage"/> + <!-- Click 'Check Out with Multiple Addresses' --> + <waitForPageLoad stepKey="waitForSecondPageLoad"/> + <actionGroup ref="StorefrontGoCheckoutWithMultipleAddresses" stepKey="goCheckoutWithMultipleAddresses"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontCheckoutShippingSelectMultipleAddressesActionGroup" stepKey="selectMultipleAddresses"> + <argument name="firstAddress" value="{{UK_Not_Default_Address.street[0]}}"/> + <argument name="secondAddress" value="{{US_Address_NY.street[1]}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitPageLoad"/> + <!-- Open the Cart page in another browser window and go back --> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnShoppingCartPageNewTab"/> + <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertFirstProductItemInCheckOutCart"> + <argument name="productName" value="$$createFirstProduct.name$$"/> + <argument name="productSku" value="$$createFirstProduct.sku$$"/> + <argument name="productPrice" value="$$createFirstProduct.price$$"/> + <argument name="subtotal" value="$$createFirstProduct.price$$" /> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSecondProductItemInCheckOutCart"> + <argument name="productName" value="$$createSecondProduct.name$$"/> + <argument name="productSku" value="$$createSecondProduct.sku$$"/> + <argument name="productPrice" value="$$createSecondProduct.price$$"/> + <argument name="subtotal" value="$$createSecondProduct.price$$" /> + <argument name="qty" value="1"/> + </actionGroup> + <switchToNextTab stepKey="switchToNextTab"/> + <!-- Click 'Continue to Billing Information' and 'Go to Review Your Order' --> + <actionGroup ref="StorefrontGoToBillingInformationActionGroup" stepKey="goToBillingInformation"/> + <see selector="{{ShipmentFormSection.shippingAddress}}" userInput="{{US_Address_NY.city}}" stepKey="seeBillingAddress"/> + <waitForElementVisible selector="{{StorefrontMultipleShippingMethodSection.goToReviewYourOrderButton}}" stepKey="waitForGoToReviewYourOrderVisible" /> + <click selector="{{StorefrontMultipleShippingMethodSection.goToReviewYourOrderButton}}" stepKey="clickToGoToReviewYourOrderButton"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <see selector="{{StorefrontMultipleShippingMethodSection.successMessage}}" userInput="Successfully ordered" stepKey="seeSuccessMessage"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('1')}}" stepKey="grabFirstOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('2')}}" stepKey="grabSecondOrderId"/> + <!-- Go to My Account > My Orders --> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPageLoad"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabFirstOrderId})}}" stepKey="seeFirstOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <!-- Go to Admin > Sales > Orders --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchFirstOrder"> + <argument name="keyword" value="$grabFirstOrderId"/> + </actionGroup> + <seeElement selector="{{AdminOrdersGridSection.orderId({$grabFirstOrderId})}}" stepKey="seeAdminFirstOrder"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchSecondOrder"> + <argument name="keyword" value="$grabSecondOrderId"/> + </actionGroup> + <seeElement selector="{{AdminOrdersGridSection.orderId({$grabSecondOrderId})}}" stepKey="seeAdminSecondOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php index 9ffef2832a6bc..42715d026e9ed 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php @@ -170,7 +170,7 @@ public function executeDataProvider() { return [ 'shipping_address_exists' => ['*/checkout/addresses', 'shipping_address', 'back/address'], - 'shipping_address_not_exist' => ['*/cart/', null, 'back/cart'] + 'shipping_address_not_exist' => ['checkout/cart/', null, 'back/cart'] ]; } diff --git a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php deleted file mode 100644 index a26f2661ebab1..0000000000000 --- a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Multishipping\Test\Unit\Controller\Checkout; - -use Magento\Multishipping\Controller\Checkout\Plugin; - -class PluginTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $cartMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteMock; - - /** - * @var Plugin - */ - protected $object; - - protected function setUp() - { - $this->cartMock = $this->createMock(\Magento\Checkout\Model\Cart::class); - $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping'] - ); - $this->cartMock->expects($this->once())->method('getQuote')->will($this->returnValue($this->quoteMock)); - $this->object = new \Magento\Multishipping\Controller\Checkout\Plugin($this->cartMock); - } - - public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void - { - $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(1); - $this->quoteMock->expects($this->once())->method('setIsMultiShipping')->with(0); - $this->cartMock->expects($this->once())->method('saveQuote'); - $this->object->beforeExecute($subject); - } - - public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void - { - $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); - $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); - $this->cartMock->expects($this->never())->method('saveQuote'); - $this->object->beforeExecute($subject); - } -} diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php index 02bc966873774..fba3245bec68d 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php @@ -11,6 +11,7 @@ use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\AddressSearchResultsInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Directory\Model\Currency; use Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderDefault; use Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderFactory; use Magento\Quote\Model\Quote\Address; @@ -44,6 +45,7 @@ use Magento\Quote\Model\ShippingAssignment; use Magento\Sales\Model\Order\Email\Sender\OrderSender; use Magento\Sales\Model\OrderFactory; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use PHPUnit_Framework_MockObject_MockObject; use \PHPUnit\Framework\TestCase; @@ -418,13 +420,16 @@ public function testSetShippingMethods() $methodsArray = [1 => 'flatrate_flatrate', 2 => 'tablerate_bestway']; $addressId = 1; $addressMock = $this->getMockBuilder(QuoteAddress::class) - ->setMethods(['getId', 'setShippingMethod']) + ->setMethods(['getId', 'setShippingMethod', 'setCollectShippingRates']) ->disableOriginalConstructor() ->getMock(); $addressMock->expects($this->once())->method('getId')->willReturn($addressId); $this->quoteMock->expects($this->once())->method('getAllShippingAddresses')->willReturn([$addressMock]); $addressMock->expects($this->once())->method('setShippingMethod')->with($methodsArray[$addressId]); + $addressMock->expects($this->once()) + ->method('setCollectShippingRates') + ->with(true); $this->quoteMock->expects($this->once()) ->method('__call') ->with('setTotalsCollectedFlag', [false]) @@ -453,7 +458,8 @@ public function testCreateOrders(): void ]; $quoteItemId = 1; $paymentProviderCode = 'checkmo'; - + $shippingPrice = '0.00'; + $currencyCode = 'USD'; $simpleProductTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\Simple::class) ->disableOriginalConstructor() ->setMethods(['getOrderOptions']) @@ -467,6 +473,17 @@ public function testCreateOrders(): void $this->getQuoteAddressesMock($quoteAddressItemMock, $addressTotal); $this->setQuoteMockData($paymentProviderCode, $shippingAddressMock, $billingAddressMock); + $currencyMock = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->setMethods([ 'convert' ]) + ->getMock(); + $currencyMock->method('convert')->willReturn($shippingPrice); + $storeMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseCurrency','getCurrentCurrencyCode' ]) + ->getMock(); + $storeMock->method('getBaseCurrency')->willReturn($currencyMock); + $storeMock->method('getCurrentCurrencyCode')->willReturn($currencyCode); $orderAddressMock = $this->createSimpleMock(\Magento\Sales\Api\Data\OrderAddressInterface::class); $orderPaymentMock = $this->createSimpleMock(\Magento\Sales\Api\Data\OrderPaymentInterface::class); $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) @@ -476,6 +493,9 @@ public function testCreateOrders(): void $orderItemMock->method('getQuoteItemId')->willReturn($quoteItemId); $orderMock = $this->getOrderMock($orderAddressMock, $orderPaymentMock, $orderItemMock); + $orderMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $orderMock->expects($this->once())->method('setBaseShippingAmount')->with($shippingPrice)->willReturnSelf(); + $orderMock->expects($this->once())->method('setShippingAmount')->with($shippingPrice)->willReturnSelf(); $this->orderFactoryMock->expects($this->once())->method('create')->willReturn($orderMock); $this->dataObjectHelperMock->expects($this->once())->method('mergeDataObjects') ->with( @@ -608,12 +628,18 @@ private function getQuoteAddressesMock($quoteAddressItemMock, int $addressTotal) )->getMock(); $shippingAddressMock->method('validate')->willReturn(true); $shippingAddressMock->method('getShippingMethod')->willReturn('carrier'); - $shippingAddressMock->method('getShippingRateByCode')->willReturn('code'); $shippingAddressMock->method('getCountryId')->willReturn('EN'); $shippingAddressMock->method('getAllItems')->willReturn([$quoteAddressItemMock]); $shippingAddressMock->method('getAddressType')->willReturn('shipping'); $shippingAddressMock->method('getGrandTotal')->willReturn($addressTotal); + $shippingRateMock = $this->getMockBuilder(Address\Rate::class) + ->disableOriginalConstructor() + ->setMethods([ 'getPrice' ]) + ->getMock(); + $shippingRateMock->method('getPrice')->willReturn('0.00'); + $shippingAddressMock->method('getShippingRateByCode')->willReturn($shippingRateMock); + $billingAddressMock = $this->getMockBuilder(Address::class) ->disableOriginalConstructor() ->setMethods(['validate']) @@ -673,6 +699,9 @@ private function getOrderMock( 'getCanSendNewEmailFlag', 'getItems', 'setShippingMethod', + 'getStore', + 'setShippingAmount', + 'setBaseShippingAmount' ] )->getMock(); $orderMock->method('setQuote')->with($this->quoteMock); diff --git a/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php new file mode 100644 index 0000000000000..02ae1a70ce801 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php @@ -0,0 +1,99 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Test\Unit\Plugin; + +use Magento\Checkout\Controller\Index\Index; +use Magento\Checkout\Model\Cart; +use Magento\Multishipping\Plugin\DisableMultishippingMode; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote; + +/** + * Class DisableMultishippingModeTest + */ +class DisableMultishippingModeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $cartMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var DisableMultishippingMode + */ + private $object; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->cartMock = $this->createMock(Cart::class); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping', 'getExtensionAttributes'] + ); + $this->cartMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($this->quoteMock)); + $this->object = new DisableMultishippingMode($this->cartMock); + } + + /** + * Tests turn off multishipping on multishipping quote. + * + * @return void + */ + public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void + { + $subject = $this->createMock(Index::class); + $extensionAttributes = $this->createPartialMock( + CartExtensionInterface::class, + ['setShippingAssignments', 'getShippingAssignments'] + ); + $extensionAttributes->method('getShippingAssignments') + ->willReturn( + $this->createMock(ShippingAssignmentInterface::class) + ); + $extensionAttributes->expects($this->once()) + ->method('setShippingAssignments') + ->with([]); + $this->quoteMock->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + $this->quoteMock->expects($this->once()) + ->method('getIsMultiShipping')->willReturn(1); + $this->quoteMock->expects($this->once()) + ->method('setIsMultiShipping') + ->with(0); + $this->cartMock->expects($this->once()) + ->method('saveQuote'); + + $this->object->beforeExecute($subject); + } + + /** + * Tests turn off multishipping on non-multishipping quote. + * + * @return void + */ + public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void + { + $subject = $this->createMock(Index::class); + $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); + $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); + $this->cartMock->expects($this->never())->method('saveQuote'); + $this->object->beforeExecute($subject); + } +} diff --git a/app/code/Magento/Multishipping/etc/di.xml b/app/code/Magento/Multishipping/etc/di.xml index 3bccf0b74bcd8..ad0d341d6b2e9 100644 --- a/app/code/Magento/Multishipping/etc/di.xml +++ b/app/code/Magento/Multishipping/etc/di.xml @@ -9,4 +9,7 @@ <type name="Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="multishipping_shipping_addresses" type="Magento\Multishipping\Model\Cart\CartTotalRepositoryPlugin" /> </type> + <type name="Magento\Quote\Model\QuoteRepository"> + <plugin name="multishipping_quote_repository" type="Magento\Multishipping\Plugin\MultishippingQuoteRepository" /> + </type> </config> diff --git a/app/code/Magento/Multishipping/etc/frontend/di.xml b/app/code/Magento/Multishipping/etc/frontend/di.xml index 0c2daaf45043e..481b95280a4a4 100644 --- a/app/code/Magento/Multishipping/etc/frontend/di.xml +++ b/app/code/Magento/Multishipping/etc/frontend/di.xml @@ -31,13 +31,13 @@ </arguments> </type> <type name="Magento\Checkout\Controller\Cart\Add"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Controller\Cart\UpdatePost"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Controller\Index\Index"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Model\Cart"> <plugin name="multishipping_session_mapper" type="Magento\Multishipping\Model\Checkout\Type\Multishipping\Plugin" sortOrder="50" /> @@ -45,4 +45,7 @@ <type name="Magento\Checkout\Controller\Cart"> <plugin name="multishipping_clear_addresses" type="Magento\Multishipping\Model\Cart\Controller\CartPlugin" sortOrder="50" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="multishipping_reset_shipping_assigment" type="Magento\Multishipping\Plugin\ResetShippingAssigment"/> + </type> </config> diff --git a/app/code/Magento/MysqlMq/Model/Driver/Bulk/Exchange.php b/app/code/Magento/MysqlMq/Model/Driver/Bulk/Exchange.php index 247a44667be06..718fba0a1a1ad 100644 --- a/app/code/Magento/MysqlMq/Model/Driver/Bulk/Exchange.php +++ b/app/code/Magento/MysqlMq/Model/Driver/Bulk/Exchange.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\MysqlMq\Model\Driver\Bulk; use Magento\Framework\MessageQueue\Bulk\ExchangeInterface; -use Magento\Framework\MessageQueue\ConfigInterface as MessageQueueConfig; +use Magento\Framework\MessageQueue\Topology\ConfigInterface as MessageQueueConfig; +use Magento\MysqlMq\Model\ConnectionTypeResolver; use Magento\MysqlMq\Model\QueueManagement; /** @@ -14,6 +16,11 @@ */ class Exchange implements ExchangeInterface { + /** + * @var ConnectionTypeResolver + */ + private $connectionTypeResolver; + /** * @var MessageQueueConfig */ @@ -27,13 +34,18 @@ class Exchange implements ExchangeInterface /** * Initialize dependencies. * + * @param ConnectionTypeResolver $connectionTypeResolver * @param MessageQueueConfig $messageQueueConfig * @param QueueManagement $queueManagement */ - public function __construct(MessageQueueConfig $messageQueueConfig, QueueManagement $queueManagement) - { + public function __construct( + ConnectionTypeResolver $connectionTypeResolver, + MessageQueueConfig $messageQueueConfig, + QueueManagement $queueManagement + ) { $this->messageQueueConfig = $messageQueueConfig; $this->queueManagement = $queueManagement; + $this->connectionTypeResolver = $connectionTypeResolver; } /** @@ -41,7 +53,20 @@ public function __construct(MessageQueueConfig $messageQueueConfig, QueueManagem */ public function enqueue($topic, array $envelopes) { - $queueNames = $this->messageQueueConfig->getQueuesByTopic($topic); + $queueNames = []; + $exchanges = $this->messageQueueConfig->getExchanges(); + foreach ($exchanges as $exchange) { + $connection = $exchange->getConnection(); + if ($this->connectionTypeResolver->getConnectionType($connection)) { + foreach ($exchange->getBindings() as $binding) { + // This only supports exact matching of topics. + if ($binding->getTopic() === $topic) { + $queueNames[] = $binding->getDestination(); + } + } + } + } + $messages = array_map( function ($envelope) { return $envelope->getBody(); diff --git a/app/code/Magento/MysqlMq/Model/Driver/Exchange.php b/app/code/Magento/MysqlMq/Model/Driver/Exchange.php index b6050c6b3d0b6..3e9b131fa8d1c 100644 --- a/app/code/Magento/MysqlMq/Model/Driver/Exchange.php +++ b/app/code/Magento/MysqlMq/Model/Driver/Exchange.php @@ -3,15 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\MysqlMq\Model\Driver; use Magento\Framework\MessageQueue\EnvelopeInterface; use Magento\Framework\MessageQueue\ExchangeInterface; -use Magento\Framework\MessageQueue\ConfigInterface as MessageQueueConfig; +use Magento\Framework\MessageQueue\Topology\ConfigInterface as MessageQueueConfig; +use Magento\MysqlMq\Model\ConnectionTypeResolver; use Magento\MysqlMq\Model\QueueManagement; +/** + * Class Exchange + */ class Exchange implements ExchangeInterface { + /** + * @var ConnectionTypeResolver + */ + private $connectionTypeResolver; + /** * @var MessageQueueConfig */ @@ -25,13 +35,18 @@ class Exchange implements ExchangeInterface /** * Initialize dependencies. * + * @param ConnectionTypeResolver $connectionTypeResolver * @param MessageQueueConfig $messageQueueConfig * @param QueueManagement $queueManagement */ - public function __construct(MessageQueueConfig $messageQueueConfig, QueueManagement $queueManagement) - { + public function __construct( + ConnectionTypeResolver $connectionTypeResolver, + MessageQueueConfig $messageQueueConfig, + QueueManagement $queueManagement + ) { $this->messageQueueConfig = $messageQueueConfig; $this->queueManagement = $queueManagement; + $this->connectionTypeResolver = $connectionTypeResolver; } /** @@ -43,7 +58,18 @@ public function __construct(MessageQueueConfig $messageQueueConfig, QueueManagem */ public function enqueue($topic, EnvelopeInterface $envelope) { - $queueNames = $this->messageQueueConfig->getQueuesByTopic($topic); + $queueNames = []; + $exchanges = $this->messageQueueConfig->getExchanges(); + foreach ($exchanges as $exchange) { + $connection = $exchange->getConnection(); + if ($this->connectionTypeResolver->getConnectionType($connection)) { + foreach ($exchange->getBindings() as $binding) { + if ($binding->getTopic() == $topic) { + $queueNames[] = $binding->getDestination(); + } + } + } + } $this->queueManagement->addMessageToQueues($topic, $envelope->getBody(), $queueNames); return null; } diff --git a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php index d50ed851b64a9..2a45eafc63f24 100644 --- a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php +++ b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php @@ -151,7 +151,7 @@ public function getMessages($queueName, $limit = null) 'queue_message_status.status IN (?)', [QueueManagement::MESSAGE_STATUS_NEW, QueueManagement::MESSAGE_STATUS_RETRY_REQUIRED] )->where('queue.name = ?', $queueName) - ->order('queue_message_status.updated_at ASC'); + ->order(['queue_message_status.updated_at ASC', 'queue_message_status.id ASC']); if ($limit) { $select->limit($limit); diff --git a/app/code/Magento/MysqlMq/Setup/Recurring.php b/app/code/Magento/MysqlMq/Setup/Recurring.php index db3a39bf5fbd0..57f3931cee8d8 100644 --- a/app/code/Magento/MysqlMq/Setup/Recurring.php +++ b/app/code/Magento/MysqlMq/Setup/Recurring.php @@ -3,12 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\MysqlMq\Setup; use Magento\Framework\Setup\InstallSchemaInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; -use Magento\Framework\MessageQueue\ConfigInterface as MessageQueueConfig; +use Magento\Framework\MessageQueue\Topology\ConfigInterface as MessageQueueConfig; /** * Class Recurring @@ -29,17 +30,17 @@ public function __construct(MessageQueueConfig $messageQueueConfig) } /** - * {@inheritdoc} + * @inheritdoc */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); - $binds = $this->messageQueueConfig->getBinds(); $queues = []; - foreach ($binds as $bind) { - $queues[] = $bind[MessageQueueConfig::BIND_QUEUE]; + foreach ($this->messageQueueConfig->getQueues() as $queue) { + $queues[] = $queue->getName(); } + $connection = $setup->getConnection(); $existingQueues = $connection->fetchCol($connection->select()->from($setup->getTable('queue'), 'name')); $queues = array_unique(array_diff($queues, $existingQueues)); diff --git a/app/code/Magento/MysqlMq/Test/Unit/Model/Driver/Bulk/ExchangeTest.php b/app/code/Magento/MysqlMq/Test/Unit/Model/Driver/Bulk/ExchangeTest.php index 452825058c9d8..2f4b1350568d1 100644 --- a/app/code/Magento/MysqlMq/Test/Unit/Model/Driver/Bulk/ExchangeTest.php +++ b/app/code/Magento/MysqlMq/Test/Unit/Model/Driver/Bulk/ExchangeTest.php @@ -25,6 +25,10 @@ class ExchangeTest extends \PHPUnit\Framework\TestCase * @var \Magento\MysqlMq\Model\Driver\Bulk\Exchange */ private $exchange; + /** + * @var \Magento\MysqlMq\Model\ConnectionTypeResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $connnectionTypeResolver; /** * Set up. @@ -33,15 +37,20 @@ class ExchangeTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->messageQueueConfig = $this->getMockBuilder(\Magento\Framework\MessageQueue\ConfigInterface::class) + $this->messageQueueConfig = $this->getMockBuilder( + \Magento\Framework\MessageQueue\Topology\ConfigInterface::class + ) ->disableOriginalConstructor()->getMock(); $this->queueManagement = $this->getMockBuilder(\Magento\MysqlMq\Model\QueueManagement::class) ->disableOriginalConstructor()->getMock(); + $this->connnectionTypeResolver = $this->getMockBuilder(\Magento\MysqlMq\Model\ConnectionTypeResolver::class) + ->disableOriginalConstructor()->getMock(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->exchange = $objectManager->getObject( \Magento\MysqlMq\Model\Driver\Bulk\Exchange::class, [ + 'connectionTypeResolver' => $this->connnectionTypeResolver, 'messageQueueConfig' => $this->messageQueueConfig, 'queueManagement' => $this->queueManagement, ] @@ -56,10 +65,46 @@ protected function setUp() public function testEnqueue() { $topicName = 'topic.name'; - $queueNames = ['queue0', 'queue1']; + $queueNames = ['queue0']; + $binding1 = $this->createMock( + \Magento\Framework\MessageQueue\Topology\Config\ExchangeConfigItem\BindingInterface::class + ); + $binding1->expects($this->once()) + ->method('getTopic') + ->willReturn($topicName); + $binding1->expects($this->once()) + ->method('getDestination') + ->willReturn($queueNames[0]); + $binding2 = $this->createMock( + \Magento\Framework\MessageQueue\Topology\Config\ExchangeConfigItem\BindingInterface::class + ); + $binding2->expects($this->once()) + ->method('getTopic') + ->willReturn('different.topic'); + $binding2->expects($this->never()) + ->method('getDestination'); + $exchange1 = $this->createMock( + \Magento\Framework\MessageQueue\Topology\Config\ExchangeConfigItemInterface::class + ); + $exchange1->expects($this->once()) + ->method('getConnection') + ->willReturn('db'); + $exchange1->expects($this->once()) + ->method('getBindings') + ->willReturn([$binding1, $binding2]); + $exchange2 = $this->createMock( + \Magento\Framework\MessageQueue\Topology\Config\ExchangeConfigItemInterface::class + ); + $exchange2->expects($this->once()) + ->method('getConnection') + ->willReturn('amqp'); + $exchange2->expects($this->never()) + ->method('getBindings'); + + $this->connnectionTypeResolver->method('getConnectionType')->willReturnOnConsecutiveCalls(['db', null]); $envelopeBody = 'serializedMessage'; $this->messageQueueConfig->expects($this->once()) - ->method('getQueuesByTopic')->with($topicName)->willReturn($queueNames); + ->method('getExchanges')->willReturn([$exchange1, $exchange2]); $envelope = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) ->disableOriginalConstructor()->getMock(); $envelope->expects($this->once())->method('getBody')->willReturn($envelopeBody); diff --git a/app/code/Magento/MysqlMq/Test/Unit/Model/ResourceModel/QueueTest.php b/app/code/Magento/MysqlMq/Test/Unit/Model/ResourceModel/QueueTest.php index d3fe09a712945..e3c6e6d9aee2a 100644 --- a/app/code/Magento/MysqlMq/Test/Unit/Model/ResourceModel/QueueTest.php +++ b/app/code/Magento/MysqlMq/Test/Unit/Model/ResourceModel/QueueTest.php @@ -206,7 +206,9 @@ public function testGetMessages() ] )->willReturnSelf(); $select->expects($this->once()) - ->method('order')->with('queue_message_status.updated_at ASC')->willReturnSelf(); + ->method('order') + ->with(['queue_message_status.updated_at ASC', 'queue_message_status.id ASC']) + ->willReturnSelf(); $select->expects($this->once())->method('limit')->with($limit)->willReturnSelf(); $connection->expects($this->once())->method('fetchAll')->with($select)->willReturn($messages); $this->assertEquals($messages, $this->queue->getMessages($queueName, $limit)); diff --git a/app/code/Magento/MysqlMq/Test/Unit/Setup/RecurringTest.php b/app/code/Magento/MysqlMq/Test/Unit/Setup/RecurringTest.php index e2e7ad3c4c92d..ccbe41a4bd705 100644 --- a/app/code/Magento/MysqlMq/Test/Unit/Setup/RecurringTest.php +++ b/app/code/Magento/MysqlMq/Test/Unit/Setup/RecurringTest.php @@ -34,7 +34,9 @@ class RecurringTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->objectManager = new ObjectManager($this); - $this->messageQueueConfig = $this->getMockBuilder(\Magento\Framework\MessageQueue\ConfigInterface::class) + $this->messageQueueConfig = $this->getMockBuilder( + \Magento\Framework\MessageQueue\Topology\ConfigInterface::class + ) ->getMockForAbstractClass(); $this->model = $this->objectManager->getObject( \Magento\MysqlMq\Setup\Recurring::class, @@ -49,23 +51,14 @@ protected function setUp() */ public function testInstall() { - $binds = [ - 'first_bind' => [ - 'queue' => 'queue_name_1', - 'exchange' => 'magento-db', - 'topic' => 'queue.topic.1' - ], - 'second_bind' => [ - 'queue' => 'queue_name_2', - 'exchange' => 'magento-db', - 'topic' => 'queue.topic.2' - ], - 'third_bind' => [ - 'queue' => 'queue_name_3', - 'exchange' => 'magento-db', - 'topic' => 'queue.topic.3' - ] - ]; + for ($i = 1; $i <= 3; $i++) { + $queue = $this->createMock(\Magento\Framework\MessageQueue\Topology\Config\QueueConfigItemInterface::class); + $queue->expects($this->once()) + ->method('getName') + ->willReturn('queue_name_' . $i); + $queues[] = $queue; + } + $dbQueues = [ 'queue_name_1', 'queue_name_2', @@ -81,7 +74,7 @@ public function testInstall() ->getMockForAbstractClass(); $setup->expects($this->once())->method('startSetup')->willReturnSelf(); - $this->messageQueueConfig->expects($this->once())->method('getBinds')->willReturn($binds); + $this->messageQueueConfig->expects($this->once())->method('getQueues')->willReturn($queues); $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->getMockForAbstractClass(); $setup->expects($this->once())->method('getConnection')->willReturn($connection); diff --git a/app/code/Magento/MysqlMq/etc/adminhtml/system.xml b/app/code/Magento/MysqlMq/etc/adminhtml/system.xml index 2684f2e0c98bf..045a176a48e87 100644 --- a/app/code/Magento/MysqlMq/etc/adminhtml/system.xml +++ b/app/code/Magento/MysqlMq/etc/adminhtml/system.xml @@ -13,15 +13,19 @@ <comment>All the times are in minutes. Use "0" if you want to skip automatic clearance.</comment> <field id="retry_inprogress_after" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Retry Messages In Progress After</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="successful_messages_lifetime" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Successful Messages Lifetime</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="failed_messages_lifetime" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Failed Messages Lifetime</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> <field id="new_messages_lifetime" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0"> <label>New Messages Lifetime</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> </group> </section> diff --git a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php index bacdd3e4a81fe..99d3f4d406adc 100644 --- a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php +++ b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php @@ -7,6 +7,9 @@ use \Magento\Framework\HTTP\ZendClient; +/** + * Performs the request to make the deployment + */ class Deployments { /** @@ -88,7 +91,7 @@ public function setDeployment($description, $change = false, $user = false) return false; } - if (($response->getStatus() < 200 || $response->getStatus() > 210)) { + if ($response->getStatus() < 200 || $response->getStatus() > 210) { $this->logger->warning('Deployment marker request did not send a 200 status code.'); return false; } diff --git a/app/code/Magento/NewRelicReporting/Model/Module/Collect.php b/app/code/Magento/NewRelicReporting/Model/Module/Collect.php index 0d8a94fbed940..fe5389e258aa5 100644 --- a/app/code/Magento/NewRelicReporting/Model/Module/Collect.php +++ b/app/code/Magento/NewRelicReporting/Model/Module/Collect.php @@ -6,7 +6,7 @@ namespace Magento\NewRelicReporting\Model\Module; use Magento\Framework\Module\FullModuleList; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\Module\ModuleListInterface; use Magento\NewRelicReporting\Model\Config; use Magento\NewRelicReporting\Model\Module; @@ -22,7 +22,7 @@ class Collect protected $moduleList; /** - * @var ModuleManagerInterface + * @var Manager */ protected $moduleManager; @@ -46,14 +46,14 @@ class Collect * * @param ModuleListInterface $moduleList * @param FullModuleList $fullModuleList - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param \Magento\NewRelicReporting\Model\ModuleFactory $moduleFactory * @param \Magento\NewRelicReporting\Model\ResourceModel\Module\CollectionFactory $moduleCollectionFactory */ public function __construct( ModuleListInterface $moduleList, FullModuleList $fullModuleList, - ModuleManagerInterface $moduleManager, + Manager $moduleManager, \Magento\NewRelicReporting\Model\ModuleFactory $moduleFactory, \Magento\NewRelicReporting\Model\ResourceModel\Module\CollectionFactory $moduleCollectionFactory ) { diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php index 3c30d95b77de0..4286406d6e9ab 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php @@ -8,6 +8,7 @@ use Magento\NewRelicReporting\Model\Module\Collect; use Magento\Framework\Module\FullModuleList; use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\Module\Manager; use Magento\NewRelicReporting\Model\Module; /** diff --git a/app/code/Magento/NewRelicReporting/etc/db_schema.xml b/app/code/Magento/NewRelicReporting/etc/db_schema.xml index c6e61b88f4b1b..e18d7c8077bb9 100644 --- a/app/code/Magento/NewRelicReporting/etc/db_schema.xml +++ b/app/code/Magento/NewRelicReporting/etc/db_schema.xml @@ -22,7 +22,7 @@ </table> <table name="reporting_module_status" resource="default" engine="innodb" comment="Module Status Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Module Id"/> + comment="Module ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Module Name"/> <column xsi:type="varchar" name="active" nullable="true" length="255" comment="Module Active Status"/> <column xsi:type="varchar" name="setup_version" nullable="true" length="255" comment="Module Version"/> diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php index 9fd9f4335b5c5..c5ed6fb55c48b 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php @@ -6,12 +6,14 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Newsletter\Controller\Adminhtml\Template; /** * View a rendered template. */ -class Preview extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpGetActionInterface +class Preview extends Template implements HttpPostActionInterface, HttpGetActionInterface { /** * Preview Newsletter template @@ -25,7 +27,7 @@ public function execute() $data = $this->getRequest()->getParams(); $isEmptyRequestData = empty($data) || !isset($data['id']); $isEmptyPreviewData = !$this->_getSession()->hasPreviewData() || empty($this->_getSession()->getPreviewData()); - + if ($isEmptyRequestData && $isEmptyPreviewData) { $this->_forward('noroute'); return $this; diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index f9e9d57bf4b40..8ca489d89c1df 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -130,7 +130,7 @@ public function loadByEmail($subscriberEmail) */ public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer) { - $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds(); + $storeIds = $this->storeManager->getWebsite()->getStoreIds(); if ($customer->getId()) { $select = $this->connection diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index 85d512afaf262..5df9feacf654b 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -353,11 +353,7 @@ public function isStatusChanged() */ public function isSubscribed() { - if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) { - return true; - } - - return false; + return $this->getId() && (int)$this->getStatus() === self::STATUS_SUBSCRIBED; } /** @@ -732,7 +728,13 @@ public function sendConfirmationRequestEmail() 'store' => $this->_storeManager->getStore()->getId(), ] )->setTemplateVars( - ['subscriber' => $this, 'store' => $this->_storeManager->getStore()] + [ + 'subscriber' => $this, + 'store' => $this->_storeManager->getStore(), + 'subscriber_data' => [ + 'confirmation_link' => $this->getConfirmationLink(), + ], + ] )->setFrom( $this->_scopeConfig->getValue( self::XML_PATH_CONFIRM_EMAIL_IDENTITY, diff --git a/app/code/Magento/Newsletter/Model/Template.php b/app/code/Magento/Newsletter/Model/Template.php index 4c71014826cf5..88fbfb152d14f 100644 --- a/app/code/Magento/Newsletter/Model/Template.php +++ b/app/code/Magento/Newsletter/Model/Template.php @@ -200,9 +200,16 @@ public function getProcessedTemplateSubject(array $variables) { $variables['this'] = $this; - return $this->getTemplateFilter() - ->setVariables($variables) - ->filter($this->getTemplateSubject()); + $filter = $this->getTemplateFilter(); + $filter->setVariables($variables); + + $previousStrictMode = $filter->setStrictMode( + !$this->getData('is_legacy') && is_numeric($this->getTemplateId()) + ); + $result = $filter->filter($this->getTemplateSubject()); + $filter->setStrictMode($previousStrictMode); + + return $result; } /** @@ -227,6 +234,8 @@ public function getTemplateText() } /** + * Return the filter factory + * * @return \Magento\Newsletter\Model\Template\FilterFactory */ protected function getFilterFactory() diff --git a/app/code/Magento/Newsletter/Setup/Patch/Data/FlagLegacyTemplates.php b/app/code/Magento/Newsletter/Setup/Patch/Data/FlagLegacyTemplates.php new file mode 100644 index 0000000000000..13baf7f1dd5d4 --- /dev/null +++ b/app/code/Magento/Newsletter/Setup/Patch/Data/FlagLegacyTemplates.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Newsletter\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Flag all existing email templates overrides as legacy + */ +class FlagLegacyTemplates implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup) + { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritDoc + */ + public function apply() + { + $this->moduleDataSetup + ->getConnection() + ->update($this->moduleDataSetup->getTable('newsletter_template'), ['is_legacy' => '1']); + } + + /** + * @inheritDoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml index 1df1cd5f8dae8..02657340bf34d 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml @@ -19,8 +19,8 @@ <data key="dataUiId">magento-newsletter-newsletter-subscriber</data> </entity> <entity name="AdminMenuMarketingCommunicationsNewsletterTemplate"> - <data key="pageTitle">Newsletter Template</data> - <data key="title">Newsletter Template</data> + <data key="pageTitle">Newsletter Templates</data> + <data key="title">Newsletter Templates</data> <data key="dataUiId">magento-newsletter-newsletter-template</data> </entity> <entity name="AdminMenuReportsMarketingNewsletterProblemReports"> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index 510f3e16e8d8e..0371c0265d149 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -16,9 +16,6 @@ <description value="Admin should be able to add image to WYSIWYG content Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84377"/> - <skip> - <issueId value="MC-17233"/> - </skip> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml index a7ac9e38d4b07..273a39a312132 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml @@ -15,10 +15,7 @@ <title value="Admin should be able to add widget to WYSIWYG Editor of Newsletter"/> <description value="Admin should be able to add widget to WYSIWYG Editor Newsletter"/> <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-84682"/> - <skip> - <issueId value="MC-17140"/> - </skip> + <testCaseId value="MC-6070"/> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="login"/> @@ -26,13 +23,14 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{NewsletterTemplateForm.url}}" stepKey="amOnNewsletterTemplatePage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForElementVisible selector="{{BasicFieldNewsletterSection.templateName}}" stepKey="waitForTemplateName"/> <fillField selector="{{BasicFieldNewsletterSection.templateName}}" userInput="{{_defaultNewsletter.name}}" stepKey="fillTemplateName" /> <fillField selector="{{BasicFieldNewsletterSection.templateSubject}}" userInput="{{_defaultNewsletter.subject}}" stepKey="fillTemplateSubject" /> <fillField selector="{{BasicFieldNewsletterSection.senderName}}" userInput="{{_defaultNewsletter.senderName}}" stepKey="fillSenderName" /> <fillField selector="{{BasicFieldNewsletterSection.senderEmail}}" userInput="{{_defaultNewsletter.senderEmail}}" stepKey="fillSenderEmail" /> <conditionalClick selector="{{NewsletterWYSIWYGSection.ShowHideBtn}}" dependentSelector="{{NewsletterWYSIWYGSection.TinyMCE4}}" visible="false" stepKey="toggleEditorIfHidden"/> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE" /> + <waitForElementVisible selector="{{NewsletterWYSIWYGSection.InsertWidgetIcon}}" stepKey="waitForInsertWidgerIconButton"/> <click selector="{{NewsletterWYSIWYGSection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> <wait time="10" stepKey="waitForPageLoad" /> <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index 22ca214c94aec..4d60b7676605e 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -49,7 +49,7 @@ </actionGroup> <magentoCLI command="indexer:reindex" stepKey="reindex"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Go to store front (default) and click Create an Account.--> diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php index 52bb803dd377f..b53ac39f0e4e2 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php @@ -197,20 +197,20 @@ protected function getModelMock(array $mockedMethods = []) public function testGetProcessedTemplateSubject() { - $model = $this->getModelMock([ - 'getTemplateFilter', - 'getDesignConfig', - 'applyDesignConfig', - 'setVariables', - ]); + $model = $this->getModelMock( + [ + 'getTemplateFilter', + 'getDesignConfig', + 'applyDesignConfig', + 'setVariables', + ] + ); $templateSubject = 'templateSubject'; $model->setTemplateSubject($templateSubject); + $model->setTemplateId('foobar'); - $filterTemplate = $this->getMockBuilder(\Magento\Framework\Filter\Template::class) - ->setMethods(['setVariables', 'setStoreId', 'filter']) - ->disableOriginalConstructor() - ->getMock(); + $filterTemplate = $this->createMock(\Magento\Framework\Filter\Template::class); $model->expects($this->once()) ->method('getTemplateFilter') ->will($this->returnValue($filterTemplate)); @@ -221,6 +221,11 @@ public function testGetProcessedTemplateSubject() ->with($templateSubject) ->will($this->returnValue($expectedResult)); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(false)], [$this->equalTo(true)]) + ->willReturnOnConsecutiveCalls(true, false); + $variables = ['key' => 'value']; $filterTemplate->expects($this->once()) ->method('setVariables') @@ -245,21 +250,24 @@ public function testGetProcessedTemplateSubject() */ public function testGetProcessedTemplate($variables, $templateType, $storeId, $expectedVariables, $expectedResult) { + class_exists(\Magento\Newsletter\Model\Template\Filter::class, true); $filterTemplate = $this->getMockBuilder(\Magento\Newsletter\Model\Template\Filter::class) - ->setMethods([ - 'setUseSessionInUrl', - 'setPlainTemplateMode', - 'setIsChildTemplate', - 'setDesignParams', - 'setVariables', - 'setStoreId', - 'filter', - 'getStoreId', - 'getInlineCssFiles', - ]) + ->setMethods( + [ + 'setUseSessionInUrl', + 'setPlainTemplateMode', + 'setIsChildTemplate', + 'setDesignParams', + 'setVariables', + 'setStoreId', + 'filter', + 'getStoreId', + 'getInlineCssFiles', + 'setStrictMode', + ] + ) ->disableOriginalConstructor() ->getMock(); - $filterTemplate->expects($this->once()) ->method('setUseSessionInUrl') ->with(false) @@ -281,12 +289,15 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e ->method('getStoreId') ->will($this->returnValue($storeId)); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(true)], [$this->equalTo(false)]) + ->willReturnOnConsecutiveCalls(false, true); + // The following block of code tests to ensure that the store id of the subscriber will be used, if the // 'subscriber' variable is set. $subscriber = $this->getMockBuilder(\Magento\Newsletter\Model\Subscriber::class) - ->setMethods([ - 'getStoreId', - ]) + ->setMethods(['getStoreId']) ->disableOriginalConstructor() ->getMock(); $subscriber->expects($this->once()) @@ -296,18 +307,20 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e $variables['subscriber'] = $subscriber; $expectedVariables['store'] = $this->store; - - $model = $this->getModelMock([ - 'getDesignParams', - 'applyDesignConfig', - 'getTemplateText', - 'isPlain', - ]); + $model = $this->getModelMock( + [ + 'getDesignParams', + 'applyDesignConfig', + 'getTemplateText', + 'isPlain', + ] + ); $filterTemplate->expects($this->any()) ->method('setVariables') ->with(array_merge(['this' => $model], $expectedVariables)); $model->setTemplateFilter($filterTemplate); $model->setTemplateType($templateType); + $model->setTemplateId('123'); $designParams = [ 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, diff --git a/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php b/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php index 9b02f60765fb1..6846231319d69 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php @@ -121,7 +121,6 @@ public function testNewsletterDisabled() : void ->willReturn(false); $expectedRedirectUrl = 'https://test.com/index'; - $this->configMock->expects($this->once()) ->method('getValue') ->with('web/default/no_route', ScopeInterface::SCOPE_STORE) diff --git a/app/code/Magento/Newsletter/etc/adminhtml/menu.xml b/app/code/Magento/Newsletter/etc/adminhtml/menu.xml index a9cedf1c7a1ee..8fc21494b5de7 100644 --- a/app/code/Magento/Newsletter/etc/adminhtml/menu.xml +++ b/app/code/Magento/Newsletter/etc/adminhtml/menu.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> - <add id="Magento_Newsletter::newsletter_template" title="Newsletter Template" translate="title" module="Magento_Newsletter" parent="Magento_Backend::marketing_communications" sortOrder="30" action="newsletter/template/" resource="Magento_Newsletter::template"/> + <add id="Magento_Newsletter::newsletter_template" title="Newsletter Templates" translate="title" module="Magento_Newsletter" parent="Magento_Backend::marketing_communications" sortOrder="30" action="newsletter/template/" resource="Magento_Newsletter::template"/> <add id="Magento_Newsletter::newsletter_queue" title="Newsletter Queue" translate="title" module="Magento_Newsletter" sortOrder="40" parent="Magento_Backend::marketing_communications" action="newsletter/queue/" resource="Magento_Newsletter::queue"/> <add id="Magento_Newsletter::newsletter_subscriber" title="Newsletter Subscribers" translate="title" module="Magento_Newsletter" sortOrder="50" parent="Magento_Backend::marketing_communications" action="newsletter/subscriber/" resource="Magento_Newsletter::subscriber"/> <add id="Magento_Newsletter::newsletter_problem" title="Newsletter Problem Reports" translate="title" module="Magento_Newsletter" sortOrder="50" parent="Magento_Reports::report_marketing" action="newsletter/problem/" resource="Magento_Newsletter::problem"/> diff --git a/app/code/Magento/Newsletter/etc/db_schema.xml b/app/code/Magento/Newsletter/etc/db_schema.xml index 5cb572f41b6be..c038b02404875 100644 --- a/app/code/Magento/Newsletter/etc/db_schema.xml +++ b/app/code/Magento/Newsletter/etc/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="newsletter_subscriber" resource="default" engine="innodb" comment="Newsletter Subscriber"> <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Subscriber Id"/> + comment="Subscriber ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="timestamp" name="change_status_at" on_update="false" nullable="true" comment="Change Status At"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Id"/> + default="0" comment="Customer ID"/> <column xsi:type="varchar" name="subscriber_email" nullable="true" length="150" comment="Subscriber Email"/> <column xsi:type="int" name="subscriber_status" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Subscriber Status"/> @@ -54,6 +54,8 @@ default="1" comment="Template Actual"/> <column xsi:type="timestamp" name="added_at" on_update="false" nullable="true" comment="Added At"/> <column xsi:type="timestamp" name="modified_at" on_update="false" nullable="true" comment="Modified At"/> + <column xsi:type="boolean" name="is_legacy" nullable="false" + default="false" comment="Should the template render in legacy mode"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="template_id"/> </constraint> @@ -69,7 +71,7 @@ </table> <table name="newsletter_queue" resource="default" engine="innodb" comment="Newsletter Queue"> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Queue Id"/> + comment="Queue ID"/> <column xsi:type="int" name="template_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Template ID"/> <column xsi:type="int" name="newsletter_type" padding="11" unsigned="false" nullable="true" identity="false" @@ -98,11 +100,11 @@ </table> <table name="newsletter_queue_link" resource="default" engine="innodb" comment="Newsletter Queue Link"> <column xsi:type="int" name="queue_link_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Queue Link Id"/> + comment="Queue Link ID"/> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Queue Id"/> + default="0" comment="Queue ID"/> <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Subscriber Id"/> + default="0" comment="Subscriber ID"/> <column xsi:type="timestamp" name="letter_sent_at" on_update="false" nullable="true" comment="Letter Sent At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_link_id"/> @@ -123,9 +125,9 @@ </table> <table name="newsletter_queue_store_link" resource="default" engine="innodb" comment="Newsletter Queue Store Link"> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Queue Id"/> + default="0" comment="Queue ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_id"/> <column name="store_id"/> @@ -142,11 +144,11 @@ </table> <table name="newsletter_problem" resource="default" engine="innodb" comment="Newsletter Problems"> <column xsi:type="int" name="problem_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Problem Id"/> + comment="Problem ID"/> <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Subscriber Id"/> + comment="Subscriber ID"/> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Queue Id"/> + default="0" comment="Queue ID"/> <column xsi:type="int" name="problem_error_code" padding="10" unsigned="true" nullable="true" identity="false" default="0" comment="Problem Error Code"/> <column xsi:type="varchar" name="problem_error_text" nullable="true" length="200" comment="Problem Error Text"/> diff --git a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml index aef84c068100a..5d54dab99e753 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml @@ -91,7 +91,6 @@ <arguments> <argument name="header" xsi:type="string" translate="true">Customer First Name</argument> <argument name="index" xsi:type="string">firstname</argument> - <argument name="default" xsi:type="string">----</argument> <argument name="header_css_class" xsi:type="string">col-first-name</argument> <argument name="column_css_class" xsi:type="string">col-first-name</argument> </arguments> @@ -100,7 +99,6 @@ <arguments> <argument name="header" xsi:type="string" translate="true">Customer Last Name</argument> <argument name="index" xsi:type="string">lastname</argument> - <argument name="default" xsi:type="string">----</argument> <argument name="header_css_class" xsi:type="string">col-last-name</argument> <argument name="column_css_class" xsi:type="string">col-last-name</argument> </arguments> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index 5175080add914..20ff63a60a263 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -17,6 +17,7 @@ <iframe name="preview_iframe" id="preview_iframe" + class="preview_iframe" frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%" diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml index c49a5c61a7172..abc56070b6892 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml @@ -17,12 +17,13 @@ use Magento\Framework\App\TemplateTypesInterface; </div> <?= /* @noEscape */ $block->getForm() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="get" id="newsletter_template_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_template_preview_form" target="_blank"> <div class="no-display"> <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> <input type="hidden" id="preview_id" name="id" value="" /> + <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" > </div> </form> <script> diff --git a/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html b/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html index eaf760c080370..beeda47d9d738 100644 --- a/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html +++ b/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html @@ -6,14 +6,13 @@ --> <!--@subject {{trans "Newsletter subscription confirmation"}} @--> <!--@vars { -"var customer.name":"Customer Name", -"var subscriber.getConfirmationLink()":"Subscriber Confirmation URL" +"var subscriber_data.confirmation_link":"Subscriber Confirmation URL" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "Thank you for subscribing to our newsletter."}}</p> <p>{{trans "To begin receiving the newsletter, you must first confirm your subscription by clicking on the following link:"}}</p> -<p><a href="{{var subscriber.getConfirmationLink()}}">{{var subscriber.getConfirmationLink()}}</a></p> +<p><a href="{{var subscriber_data.confirmation_link}}">{{var subscriber_data.confirmation_link}}</a></p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/OfflinePayments/Observer/BeforeOrderPaymentSaveObserver.php b/app/code/Magento/OfflinePayments/Observer/BeforeOrderPaymentSaveObserver.php index b177b115d02ab..daf1cef3fff60 100644 --- a/app/code/Magento/OfflinePayments/Observer/BeforeOrderPaymentSaveObserver.php +++ b/app/code/Magento/OfflinePayments/Observer/BeforeOrderPaymentSaveObserver.php @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -/** - * OfflinePayments Observer - */ namespace Magento\OfflinePayments\Observer; use Magento\Framework\Event\ObserverInterface; @@ -14,6 +11,9 @@ use Magento\OfflinePayments\Model\Cashondelivery; use Magento\OfflinePayments\Model\Checkmo; +/** + * Sets payment additional information. + */ class BeforeOrderPaymentSaveObserver implements ObserverInterface { /** diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml index 36f9d35327fce..28395f8eeb849 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml @@ -7,8 +7,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> <?php if ($block->getInfo()->getAdditionalInformation()) : ?> <?php if ($block->getPayableTo()) : ?> <br /><?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml index d8d952526e67b..f85a8f8357dd9 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml @@ -7,8 +7,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> {{pdf_row_separator}} <?php if ($block->getInfo()->getAdditionalInformation()) : ?> {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml index 2a6de7f0cc356..ae7f654a1350b 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml @@ -6,8 +6,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<div class="order-payment-method-name"><?= $block->escapeHtml($block->getMethod()->getTitle()) ?></div> +<div class="order-payment-method-name"><?= $block->escapeHtml($paymentTitle) ?></div> <table class="data-table admin__table-secondary"> <tr> <th><?= $block->escapeHtml(__('Purchase Order Number')) ?>:</th> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Data/MultishippingConfigData.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Data/MultishippingConfigData.xml new file mode 100644 index 0000000000000..569eddf336782 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Data/MultishippingConfigData.xml @@ -0,0 +1,41 @@ +<?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="EnableFreeShippingMethod"> + <data key="path">carriers/freeshipping/active</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableFreeShippingMethod"> + <data key="path">carriers/freeshipping/active</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableFlatRateShippingMethod"> + <data key="path">carriers/flatrate/active</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableFlatRateShippingMethod"> + <data key="path">carriers/flatrate/active</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableMultiShippingCheckoutMultiple"> + <data key="path">multishipping/options/checkout_multiple</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableMultiShippingCheckoutMultiple"> + <data key="path">multishipping/options/checkout_multiple</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/OfflineShipping/etc/db_schema.xml b/app/code/Magento/OfflineShipping/etc/db_schema.xml index 5129e8a29b2a1..6bda6597e2f61 100644 --- a/app/code/Magento/OfflineShipping/etc/db_schema.xml +++ b/app/code/Magento/OfflineShipping/etc/db_schema.xml @@ -11,11 +11,11 @@ <column xsi:type="int" name="pk" padding="10" unsigned="true" nullable="false" identity="true" comment="Primary key"/> <column xsi:type="int" name="website_id" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="varchar" name="dest_country_id" nullable="false" length="4" default="0" comment="Destination coutry ISO/2 or ISO/3 code"/> <column xsi:type="int" name="dest_region_id" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Destination Region Id"/> + default="0" comment="Destination Region ID"/> <column xsi:type="varchar" name="dest_zip" nullable="false" length="10" default="*" comment="Destination Post Code (Zip)"/> <column xsi:type="varchar" name="condition_name" nullable="false" length="30" comment="Rate Condition name"/> diff --git a/app/code/Magento/PageCache/Model/DepersonalizeChecker.php b/app/code/Magento/PageCache/Model/DepersonalizeChecker.php index 4012499d5da5a..3023efb7a71a6 100644 --- a/app/code/Magento/PageCache/Model/DepersonalizeChecker.php +++ b/app/code/Magento/PageCache/Model/DepersonalizeChecker.php @@ -20,7 +20,7 @@ class DepersonalizeChecker /** * Module manager * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManager; @@ -33,12 +33,12 @@ class DepersonalizeChecker /** * @param \Magento\Framework\App\RequestInterface $request - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param Config $cacheConfig */ public function __construct( \Magento\Framework\App\RequestInterface $request, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, Config $cacheConfig ) { $this->request = $request; diff --git a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php index ef85d6faf2371..88619673ad425 100644 --- a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php +++ b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php @@ -7,6 +7,8 @@ /** * Class LayoutPlugin + * + * Plugin for Magento\Framework\View\Layout */ class LayoutPlugin { @@ -20,22 +22,31 @@ class LayoutPlugin */ protected $response; + /** + * @var \Magento\Framework\App\MaintenanceMode + */ + private $maintenanceMode; + /** * Constructor * * @param \Magento\Framework\App\ResponseInterface $response - * @param \Magento\PageCache\Model\Config $config + * @param \Magento\PageCache\Model\Config $config + * @param \Magento\Framework\App\MaintenanceMode $maintenanceMode */ public function __construct( \Magento\Framework\App\ResponseInterface $response, - \Magento\PageCache\Model\Config $config + \Magento\PageCache\Model\Config $config, + \Magento\Framework\App\MaintenanceMode $maintenanceMode ) { $this->response = $response; $this->config = $config; + $this->maintenanceMode = $maintenanceMode; } /** * Set appropriate Cache-Control headers + * * We have to set public headers in order to tell Varnish and Builtin app that page should be cached * * @param \Magento\Framework\View\Layout $subject @@ -44,7 +55,7 @@ public function __construct( */ public function afterGenerateXml(\Magento\Framework\View\Layout $subject, $result) { - if ($subject->isCacheable() && $this->config->isEnabled()) { + if ($subject->isCacheable() && !$this->maintenanceMode->isOn() && $this->config->isEnabled()) { $this->response->setPublicHeaders($this->config->getTtl()); } return $result; @@ -68,6 +79,7 @@ public function afterGetOutput(\Magento\Framework\View\Layout $subject, $result) if ($isVarnish && $isEsiBlock) { continue; } + // phpcs:ignore $tags = array_merge($tags, $block->getIdentities()); } } diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php deleted file mode 100644 index 7017da27eee93..0000000000000 --- a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\PageCache\Observer; - -use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\Event\Observer; -use Magento\Framework\App\Cache\Manager; -use Magento\PageCache\Model\Cache\Type as PageCacheType; -use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance\PageCacheState; - -/** - * Switch Page Cache on maintenance. - */ -class SwitchPageCacheOnMaintenance implements ObserverInterface -{ - /** - * @var Manager - */ - private $cacheManager; - - /** - * @var PageCacheState - */ - private $pageCacheStateStorage; - - /** - * @param Manager $cacheManager - * @param PageCacheState $pageCacheStateStorage - */ - public function __construct(Manager $cacheManager, PageCacheState $pageCacheStateStorage) - { - $this->cacheManager = $cacheManager; - $this->pageCacheStateStorage = $pageCacheStateStorage; - } - - /** - * Switches Full Page Cache. - * - * Depending on enabling or disabling Maintenance Mode it turns off or restores Full Page Cache state. - * - * @param Observer $observer - * @return void - */ - public function execute(Observer $observer): void - { - if ($observer->getData('isOn')) { - $this->pageCacheStateStorage->save($this->isFullPageCacheEnabled()); - $this->turnOffFullPageCache(); - } else { - $this->restoreFullPageCacheState(); - } - } - - /** - * Turns off Full Page Cache. - * - * @return void - */ - private function turnOffFullPageCache(): void - { - if (!$this->isFullPageCacheEnabled()) { - return; - } - - $this->cacheManager->clean([PageCacheType::TYPE_IDENTIFIER]); - $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], false); - } - - /** - * Full Page Cache state. - * - * @return bool - */ - private function isFullPageCacheEnabled(): bool - { - $cacheStatus = $this->cacheManager->getStatus(); - - if (!array_key_exists(PageCacheType::TYPE_IDENTIFIER, $cacheStatus)) { - return false; - } - - return (bool)$cacheStatus[PageCacheType::TYPE_IDENTIFIER]; - } - - /** - * Restores Full Page Cache state. - * - * Returns FPC to previous state that was before maintenance mode turning on. - * - * @return void - */ - private function restoreFullPageCacheState(): void - { - $storedPageCacheState = $this->pageCacheStateStorage->isEnabled(); - $this->pageCacheStateStorage->flush(); - - if ($storedPageCacheState) { - $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], true); - } - } -} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php index e4cadf728f2ea..da6a71a0c2655 100644 --- a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -13,7 +13,11 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** - * Page Cache state. + * Class PageCacheState + * + * Page Cache State Observer + * + * @deprecated Originally used by now removed observer SwitchPageCacheOnMaintenance */ class PageCacheState { diff --git a/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php b/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php index 6cdc500aaf33c..36a20ec658b6f 100644 --- a/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php +++ b/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php @@ -10,10 +10,10 @@ namespace Magento\PageCache\Plugin; use Magento\Framework\App\PageCache\FormKey as CacheFormKey; -use Magento\Framework\Escaper; use Magento\Framework\Data\Form\FormKey; -use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Escaper; use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; /** * Allow for registration of a form key through cookies. @@ -46,7 +46,7 @@ class RegisterFormKeyFromCookie private $sessionConfig; /** - * @param CacheFormKey $formKey + * @param CacheFormKey $cacheFormKey * @param Escaper $escaper * @param FormKey $formKey * @param CookieMetadataFactory $cookieMetadataFactory @@ -70,7 +70,6 @@ public function __construct( * Set form key from the cookie. * * @return void - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeDispatch(): void @@ -85,6 +84,8 @@ public function beforeDispatch(): void } /** + * Set form key cookie + * * @param string $formKey * @return void */ @@ -94,6 +95,7 @@ private function updateCookieFormKey(string $formKey): void ->createPublicCookieMetadata(); $cookieMetadata->setDomain($this->sessionConfig->getCookieDomain()); $cookieMetadata->setPath($this->sessionConfig->getCookiePath()); + $cookieMetadata->setSecure($this->sessionConfig->getCookieSecure()); $lifetime = $this->sessionConfig->getCookieLifetime(); if ($lifetime !== 0) { $cookieMetadata->setDuration($lifetime); diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml new file mode 100644 index 0000000000000..375211e5f2f51 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -0,0 +1,89 @@ +<?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="AdminFrontendAreaSessionMustNotAffectAdminAreaTest"> + <annotations> + <stories value="Backend"/> + <features value="Session cookies"/> + <title value="Frontend area session must not affect admin area"/> + <description value="Frontend area session must not affect admin area"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12353"/> + <group value="backend"/> + <group value="pagecache"/> + <group value="cookie"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="_defaultCategory" stepKey="createCategoryA"/> + <createData entity="SubCategoryWithParent" stepKey="createCategoryB"> + <requiredEntity createDataKey="createCategoryA"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryC"> + <requiredEntity createDataKey="createCategoryB"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct3"> + <requiredEntity createDataKey="createCategoryA"/> + </createData> + + <magentoCLI command="cache:clean" arguments="full_page" stepKey="clearCache"/> + <actionGroup ref="logout" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> + + <deleteData createDataKey="createCategoryC" stepKey="deleteCategoryC"/> + <deleteData createDataKey="createCategoryB" stepKey="deleteCategoryB"/> + <deleteData createDataKey="createCategoryA" stepKey="deleteCategoryA"/> + + <actionGroup ref="logout" stepKey="logoutAdmin"/> + </after> + + <!-- 1. Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- 2. Navigate Go to "Catalog"->"Products" --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="onCatalogProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- 3. Open separate tab with Storefront --> + <openNewTab stepKey="openNewTab"/> + + <!-- 4. Navigate to Men -> "Tops" -> "Jackets" --> + <amOnPage + url="{{StorefrontCategoryPage.url($$createCategoryA.custom_attributes[url_key]$$/$$createCategoryB.custom_attributes[url_key]$$/$$createCategoryC.custom_attributes[url_key]$$)}}" + stepKey="openCategoryPage"/> + <waitForPageLoad time="60" stepKey="waitForCategoryPage"/> + + <!-- 5. Open admin tab with page with products. Reload this page twice. --> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <reloadPage stepKey="reloadAdminCatalogPageFirst"/> + <waitForPageLoad stepKey="waitForReloadFirst"/> + <reloadPage stepKey="reloadAdminCatalogPageSecond"/> + <waitForPageLoad stepKey="waitForReloadSecond"/> + + <seeInTitle userInput="Products / Inventory / Catalog / Magento Admin" stepKey="seeAdminProductsPageTitle"/> + <see userInput="Products" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeAdminProductsPageHeader"/> + + <switchToNextTab stepKey="switchToFrontendTab"/> + <closeTab stepKey="closeFrontendTab"/> + </test> +</tests> diff --git a/app/code/Magento/PageCache/Test/Unit/Model/DepersonalizeCheckerTest.php b/app/code/Magento/PageCache/Test/Unit/Model/DepersonalizeCheckerTest.php index 6857c637bab84..8cc933853a492 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/DepersonalizeCheckerTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/DepersonalizeCheckerTest.php @@ -18,7 +18,7 @@ class DepersonalizeCheckerTest extends \PHPUnit\Framework\TestCase private $requestMock; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManagerMock; @@ -30,7 +30,7 @@ class DepersonalizeCheckerTest extends \PHPUnit\Framework\TestCase public function setup() { $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class); + $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); $this->cacheConfigMock = $this->createMock(\Magento\PageCache\Model\Config::class); } diff --git a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php index 6c39fe1e7979c..e2bc7f237ab0a 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php @@ -27,6 +27,11 @@ class LayoutPluginTest extends \PHPUnit\Framework\TestCase */ protected $configMock; + /** + * @var \Magento\Framework\App\MaintenanceMode|\PHPUnit\Framework\MockObject\MockObject + */ + private $maintenanceModeMock; + protected function setUp() { $this->layoutMock = $this->getMockForAbstractClass( @@ -40,27 +45,33 @@ protected function setUp() ); $this->responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); $this->configMock = $this->createMock(\Magento\PageCache\Model\Config::class); + $this->maintenanceModeMock = $this->createMock(\Magento\Framework\App\MaintenanceMode::class); $this->model = new \Magento\PageCache\Model\Layout\LayoutPlugin( $this->responseMock, - $this->configMock + $this->configMock, + $this->maintenanceModeMock ); } /** * @param $cacheState * @param $layoutIsCacheable + * @param $maintenanceModeIsEnabled + * * @dataProvider afterGenerateXmlDataProvider */ - public function testAfterGenerateXml($cacheState, $layoutIsCacheable) + public function testAfterGenerateXml($cacheState, $layoutIsCacheable, $maintenanceModeIsEnabled) { $maxAge = 180; $result = 'test'; $this->layoutMock->expects($this->once())->method('isCacheable')->will($this->returnValue($layoutIsCacheable)); $this->configMock->expects($this->any())->method('isEnabled')->will($this->returnValue($cacheState)); + $this->maintenanceModeMock->expects($this->any())->method('isOn') + ->will($this->returnValue($maintenanceModeIsEnabled)); - if ($layoutIsCacheable && $cacheState) { + if ($layoutIsCacheable && $cacheState && !$maintenanceModeIsEnabled) { $this->configMock->expects($this->once())->method('getTtl')->will($this->returnValue($maxAge)); $this->responseMock->expects($this->once())->method('setPublicHeaders')->with($maxAge); } else { @@ -76,10 +87,11 @@ public function testAfterGenerateXml($cacheState, $layoutIsCacheable) public function afterGenerateXmlDataProvider() { return [ - 'Full_cache state is true, Layout is cache-able' => [true, true], - 'Full_cache state is true, Layout is not cache-able' => [true, false], - 'Full_cache state is false, Layout is not cache-able' => [false, false], - 'Full_cache state is false, Layout is cache-able' => [false, true] + 'Full_cache state is true, Layout is cache-able' => [true, true, false], + 'Full_cache state is true, Layout is not cache-able' => [true, false, false], + 'Full_cache state is false, Layout is not cache-able' => [false, false, false], + 'Full_cache state is false, Layout is cache-able' => [false, true, false], + 'Full_cache state is true, Layout is cache-able, Maintenance mode is enabled' => [true, true, true], ]; } diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php deleted file mode 100644 index 2dbb815c70925..0000000000000 --- a/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\PageCache\Test\Unit\Observer; - -use PHPUnit\Framework\TestCase; -use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\App\Cache\Manager; -use Magento\Framework\Event\Observer; -use Magento\PageCache\Model\Cache\Type as PageCacheType; -use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance\PageCacheState; - -/** - * SwitchPageCacheOnMaintenance observer test. - */ -class SwitchPageCacheOnMaintenanceTest extends TestCase -{ - /** - * @var SwitchPageCacheOnMaintenance - */ - private $model; - - /** - * @var Manager|\PHPUnit\Framework\MockObject\MockObject - */ - private $cacheManager; - - /** - * @var PageCacheState|\PHPUnit\Framework\MockObject\MockObject - */ - private $pageCacheStateStorage; - - /** - * @var Observer|\PHPUnit\Framework\MockObject\MockObject - */ - private $observer; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $this->cacheManager = $this->createMock(Manager::class); - $this->pageCacheStateStorage = $this->createMock(PageCacheState::class); - $this->observer = $this->createMock(Observer::class); - - $this->model = $objectManager->getObject(SwitchPageCacheOnMaintenance::class, [ - 'cacheManager' => $this->cacheManager, - 'pageCacheStateStorage' => $this->pageCacheStateStorage, - ]); - } - - /** - * Tests execute when setting maintenance mode to on. - * - * @param array $cacheStatus - * @param bool $cacheState - * @param int $flushCacheCalls - * @return void - * @dataProvider enablingPageCacheStateProvider - */ - public function testExecuteWhileMaintenanceEnabling( - array $cacheStatus, - bool $cacheState, - int $flushCacheCalls - ): void { - $this->observer->method('getData') - ->with('isOn') - ->willReturn(true); - $this->cacheManager->method('getStatus') - ->willReturn($cacheStatus); - - // Page Cache state will be stored. - $this->pageCacheStateStorage->expects($this->once()) - ->method('save') - ->with($cacheState); - - // Page Cache will be cleaned and disabled - $this->cacheManager->expects($this->exactly($flushCacheCalls)) - ->method('clean') - ->with([PageCacheType::TYPE_IDENTIFIER]); - $this->cacheManager->expects($this->exactly($flushCacheCalls)) - ->method('setEnabled') - ->with([PageCacheType::TYPE_IDENTIFIER], false); - - $this->model->execute($this->observer); - } - - /** - * Tests execute when setting Maintenance Mode to off. - * - * @param bool $storedCacheState - * @param int $enableCacheCalls - * @return void - * @dataProvider disablingPageCacheStateProvider - */ - public function testExecuteWhileMaintenanceDisabling(bool $storedCacheState, int $enableCacheCalls): void - { - $this->observer->method('getData') - ->with('isOn') - ->willReturn(false); - - $this->pageCacheStateStorage->method('isEnabled') - ->willReturn($storedCacheState); - - // Nullify Page Cache state. - $this->pageCacheStateStorage->expects($this->once()) - ->method('flush'); - - // Page Cache will be enabled. - $this->cacheManager->expects($this->exactly($enableCacheCalls)) - ->method('setEnabled') - ->with([PageCacheType::TYPE_IDENTIFIER]); - - $this->model->execute($this->observer); - } - - /** - * Page Cache state data provider. - * - * @return array - */ - public function enablingPageCacheStateProvider(): array - { - return [ - 'page_cache_is_enable' => [ - 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 1], - 'cache_state' => true, - 'flush_cache_calls' => 1, - ], - 'page_cache_is_missing_in_system' => [ - 'cache_status' => [], - 'cache_state' => false, - 'flush_cache_calls' => 0, - ], - 'page_cache_is_disable' => [ - 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 0], - 'cache_state' => false, - 'flush_cache_calls' => 0, - ], - ]; - } - - /** - * Page Cache state data provider. - * - * @return array - */ - public function disablingPageCacheStateProvider(): array - { - return [ - ['stored_cache_state' => true, 'enable_cache_calls' => 1], - ['stored_cache_state' => false, 'enable_cache_calls' => 0], - ]; - } -} diff --git a/app/code/Magento/PageCache/etc/adminhtml/system.xml b/app/code/Magento/PageCache/etc/adminhtml/system.xml index 8013ad40ef5aa..234e3e48a95d8 100644 --- a/app/code/Magento/PageCache/etc/adminhtml/system.xml +++ b/app/code/Magento/PageCache/etc/adminhtml/system.xml @@ -74,6 +74,7 @@ </group> <field id="ttl" type="text" translate="label comment" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>TTL for public content</label> + <validate>validate-zero-or-greater validate-digits</validate> <comment>Public content cache lifetime in seconds. If field is empty default value 86400 will be saved. </comment> <backend_model>Magento\PageCache\Model\System\Config\Backend\Ttl</backend_model> </field> diff --git a/app/code/Magento/PageCache/etc/events.xml b/app/code/Magento/PageCache/etc/events.xml index 3f0a2532ae60a..7584f5f36d69c 100644 --- a/app/code/Magento/PageCache/etc/events.xml +++ b/app/code/Magento/PageCache/etc/events.xml @@ -57,7 +57,4 @@ <event name="customer_logout"> <observer name="FlushFormKey" instance="Magento\PageCache\Observer\FlushFormKey"/> </event> - <event name="maintenance_mode_changed"> - <observer name="page_cache_switcher_for_maintenance" instance="Magento\PageCache\Observer\SwitchPageCacheOnMaintenance"/> - </event> </config> diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js index 735fe9a6cb236..41a32ab8a49c8 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js @@ -112,11 +112,14 @@ define([ * @private */ _create: function () { - var formKey = $.mage.cookies.get('form_key'); + var formKey = $.mage.cookies.get('form_key'), + options = { + secure: window.cookiesConfig ? window.cookiesConfig.secure : false + }; if (!formKey) { formKey = generateRandomString(this.options.allowedCharacters, this.options.length); - $.mage.cookies.set('form_key', formKey); + $.mage.cookies.set('form_key', formKey, options); } $(this.options.inputSelector).val(formKey); } diff --git a/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml b/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml index 14c8bd0fecde7..eeae53d082443 100644 --- a/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml +++ b/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml @@ -11,4 +11,8 @@ <entity name="PaymentMethodCheckMoneyOrder" type="payment_method"> <data key="method">checkmo</data> </entity> + + <entity name="CashOnDeliveryPaymentMethodDefault" type="cashondelivery_payment_method"> + <requiredEntity type="active">CashOnDeliveryEnableConfigData</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml b/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml index 39506a682038f..3ad3a0e1c60de 100644 --- a/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml +++ b/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml @@ -11,4 +11,18 @@ <operation name="CreatePaymentMethod" dataType="payment_method" type="create"> <field key="method">string</field> </operation> + <operation name="cashondeliveryPaymentMethodSetup" dataType="cashondelivery_payment_method" type="create" auth="adminFormKey" url="/admin/system_config/save/section/payment/" method="POST"> + <object key="groups" dataType="cashondelivery_payment_method"> + <object key="cashondelivery" dataType="cashondelivery_payment_method"> + <object key="fields" dataType="cashondelivery_payment_method"> + <object key="active" dataType="active"> + <field key="value">string</field> + </object> + <object key="title" dataType="title"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> </operations> diff --git a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php index fbf80de519f9f..71e0384c72f79 100644 --- a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php +++ b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Payment\Ui\Component\Listing\Column\Method; /** @@ -41,6 +42,14 @@ public function toOptionArray() if ($this->options === null) { $this->options = $this->paymentHelper->getPaymentMethodList(true, true); } + + array_walk( + $this->options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); + return $this->options; } } diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml index 8b9c37f112560..3cd88bddbfb1f 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml @@ -9,8 +9,9 @@ * @see \Magento\Payment\Block\Info */ $specificInfo = $block->getSpecificInformation(); +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> <?php if ($specificInfo) : ?> <table class="data-table admin__table-secondary"> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml index a8583ea5549fe..54b9e48d07a94 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml @@ -8,8 +8,9 @@ * @see \Magento\Payment\Block\Info * @var \Magento\Payment\Block\Info $block */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} +<?= $block->escapeHtml($paymentTitle) ?>{{pdf_row_separator}} <?php if ($specificInfo = $block->getSpecificInformation()) : ?> <?php foreach ($specificInfo as $label => $value) : ?> diff --git a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js index c41be40cba144..746a4bd2cf33b 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js @@ -4,23 +4,15 @@ */ /* @api */ -(function (factory) { - 'use strict'; - - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'Magento_Payment/js/model/credit-card-validation/cvv-validator', - 'Magento_Payment/js/model/credit-card-validation/credit-card-number-validator', - 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-year-validator', - 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-month-validator', - 'Magento_Payment/js/model/credit-card-validation/credit-card-data', - 'mage/translate' - ], factory); - } else { - factory(jQuery); - } -}(function ($, cvvValidator, creditCardNumberValidator, yearValidator, monthValidator, creditCardData) { +define([ + 'jquery', + 'Magento_Payment/js/model/credit-card-validation/cvv-validator', + 'Magento_Payment/js/model/credit-card-validation/credit-card-number-validator', + 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-year-validator', + 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-month-validator', + 'Magento_Payment/js/model/credit-card-validation/credit-card-data', + 'mage/translate' +], function ($, cvvValidator, creditCardNumberValidator, yearValidator, monthValidator, creditCardData) { 'use strict'; $('.payment-method-content input[type="number"]').on('keyup', function () { @@ -111,4 +103,4 @@ rule.unshift(i); $.validator.addMethod.apply($.validator, rule); }); -})); +}); diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php index 7ad8fe658ec16..895cdb8d4c600 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Action\Action as AppAction; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; /** @@ -98,6 +100,11 @@ abstract class AbstractExpress extends AppAction implements */ protected $_customerUrl; + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Customer\Model\Session $customerSession @@ -107,6 +114,7 @@ abstract class AbstractExpress extends AppAction implements * @param \Magento\Framework\Session\Generic $paypalSession * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Customer\Model\Url $customerUrl + * @param CartRepositoryInterface $quoteRepository */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -116,7 +124,8 @@ public function __construct( \Magento\Paypal\Model\Express\Checkout\Factory $checkoutFactory, \Magento\Framework\Session\Generic $paypalSession, \Magento\Framework\Url\Helper\Data $urlHelper, - \Magento\Customer\Model\Url $customerUrl + \Magento\Customer\Model\Url $customerUrl, + CartRepositoryInterface $quoteRepository = null ) { $this->_customerSession = $customerSession; $this->_checkoutSession = $checkoutSession; @@ -128,6 +137,7 @@ public function __construct( parent::__construct($context); $parameters = ['params' => [$this->_configMethod]]; $this->_config = $this->_objectManager->create($this->_configType, $parameters); + $this->quoteRepository = $quoteRepository ?: ObjectManager::getInstance()->get(CartRepositoryInterface::class); } /** @@ -233,7 +243,12 @@ protected function _getCheckoutSession() protected function _getQuote() { if (!$this->_quote) { - $this->_quote = $this->_getCheckoutSession()->getQuote(); + if ($this->_getSession()->getQuoteId()) { + $this->_quote = $this->quoteRepository->get($this->_getSession()->getQuoteId()); + $this->_getCheckoutSession()->replaceQuote($this->_quote); + } else { + $this->_quote = $this->_getCheckoutSession()->getQuote(); + } } return $this->_quote; } @@ -243,7 +258,7 @@ protected function _getQuote() */ public function getCustomerBeforeAuthUrl() { - return; + return null; } /** diff --git a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php index 62f4c4c4c457a..0d7ec3fc6f32d 100644 --- a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php +++ b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php @@ -155,6 +155,7 @@ public function execute(): ResultInterface } else { $responseContent['redirectUrl'] = $this->urlBuilder->getUrl('paypal/express/review'); $this->_checkoutSession->setQuoteId($quote->getId()); + $this->_getSession()->setQuoteId($quote->getId()); } } catch (ApiProcessableException $e) { $responseContent['success'] = false; diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php index ede9cacf25d40..59e4db6d84201 100644 --- a/app/code/Magento/Paypal/Model/SmartButtonConfig.php +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -83,7 +83,7 @@ public function getConfig(string $page): array 'allowedFunding' => $this->getAllowedFunding($page), 'disallowedFunding' => $this->getDisallowedFunding(), 'styles' => $this->getButtonStyles($page), - 'isVisibleOnProductPage' => $this->config->getValue('visible_on_product'), + 'isVisibleOnProductPage' => (bool)$this->config->getValue('visible_on_product'), 'isGuestCheckoutAllowed' => $isGuestCheckoutAllowed ]; } diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml index c047633e5227e..4d752d8377640 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml @@ -16,7 +16,31 @@ <argument name="credentials" defaultValue="_CREDS"/> <argument name="countryCode" type="string" defaultValue="us"/> </arguments> - + + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.email(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_business_account}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod(countryCode)}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.username(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_username}}" stepKey="inputAPIUsername"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.password(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_password}}" stepKey="inputAPIPassword"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_signature}}" stepKey="inputAPISignature"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_merchant_id}}" stepKey="inputMerchantID"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + </actionGroup> + + <actionGroup name="SampleConfigPayPalExpressCheckout"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalExpressConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> @@ -29,42 +53,44 @@ <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.paypal_express_merchantID}}" stepKey="inputMerchantID"/> - <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> </actionGroup> - + <actionGroup name="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" extends="CreateOrderToPrintPageActionGroup"> <annotations> <description>EXTENDS: CreateOrderToPrintPageActionGroup. Clicks on PayPal. Fills the PayPay details in the modal. PLEASE NOTE: The PayPal Payment credentials are Hardcoded using 'Payer'.</description> </annotations> - + <arguments> + <argument name="payerName" defaultValue="MPI" type="string"/> + <argument name="credentials" defaultValue="_CREDS"/> + </arguments> + + <!-- click on PayPal payment radio button --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="clickPlaceOrder"/> - + <!--set ID for iframe of PayPal group button--> <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> - + <!--switch to iframe of PayPal group button--> - <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> <click selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="clickPayPalBtn"/> <switchToIFrame stepKey="switchBack1"/> - + <!--Check in-context--> - <comment userInput="Check in-context" stepKey="commentVerifyInContext"/> <switchToNextTab stepKey="switchToInContentTab"/> <waitForPageLoad stepKey="waitForPageLoad"/> <seeCurrentUrlMatches regex="~\//www.sandbox.paypal.com/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> - <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm"/> - <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{Payer.buyerEmail}}" stepKey="fillEmail"/> - <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{Payer.buyerPassword}}" stepKey="fillPassword"/> + <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> <waitForPageLoad stepKey="wait"/> - <seeElement selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> + <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> </actionGroup> - + <actionGroup name="addProductToCheckoutPage"> <annotations> <description>Goes to the provided Category page on the Storefront. Adds the 1st Product to the Cart. Goes to Checkout. Select the Shipping Method. Selects PayPal as the Payment Method.</description> @@ -72,7 +98,7 @@ <arguments> <argument name="Category"/> </arguments> - + <amOnPage url="{{StorefrontCategoryPage.url(Category.name)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml new file mode 100644 index 0000000000000..392014d876e46 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontPayOrderOnPayPalCheckoutActionGroup"> + <annotations> + <description>Verifies product name on Paypal cart and clicks 'Pay Now' on PayPal payment checkout page.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> + <seeElement selector="{{PayPalPaymentSection.itemName(productName)}}" stepKey="seeProductName"/> + <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml new file mode 100644 index 0000000000000..0744207494108 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml @@ -0,0 +1,52 @@ +<?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="StorefrontPaypalEnableConfigData"> + <data key="path">payment/paypal_express/active</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableConfigData"> + <data key="path">payment/paypal_express/active</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalMerchantAccountIdConfigData"> + <data key="path">payment/paypal_express/merchant_id</data> + <data key="scope_id">1</data> + <data key="value">''</data> + </entity> + <entity name="StorefrontPaypalEnableSkipOrderReviewStepConfigData"> + <data key="path">payment/paypal_express/skip_order_review_step</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableSkipOrderReviewStepConfigData"> + <data key="path">payment/paypal_express/skip_order_review_step</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalEnableInContextCheckoutConfigData"> + <data key="path">payment/paypal_express/in_context</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableInContextCheckoutConfigData"> + <data key="path">payment/paypal_express/active</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index ae34476e9ac0b..ba56243fdb391 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml @@ -99,4 +99,37 @@ <data key="paypal_express_api_signature">someApiSignature</data> <data key="paypal_express_merchantID">someMerchantId</data> </entity> + <entity name="PaypalConfig" type="paypal_config_state"> + <requiredEntity type="business_account">BusinessAccount</requiredEntity> + <requiredEntity type="api_username">ApiUsername</requiredEntity> + <requiredEntity type="api_password">ApiPassword</requiredEntity> + <requiredEntity type="api_signature">ApiSignature</requiredEntity> + <requiredEntity type="api_authentication">ApiAuthentication</requiredEntity> + <requiredEntity type="sandbox_flag">SandboxFlag</requiredEntity> + <requiredEntity type="use_proxy">UseProxy</requiredEntity> + </entity> + <entity name="BusinessAccount" type="business_account"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_business_account}}</data> + </entity> + <entity name="ApiUsername" type="api_username"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_api_username}}</data> + </entity> + <entity name="ApiPassword" type="api_password"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_api_password}}</data> + </entity> + <entity name="ApiSignature" type="api_signature"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_api_signature}}</data> + </entity> + <entity name="ApiAuthentication" type="api_authentication"> + <data key="value">0</data> + </entity> + <entity name="SandboxFlag" type="sandbox_flag"> + <data key="value">1</data> + </entity> + <entity name="UseProxy" type="use_proxy"> + <data key="value">0</data> + </entity> + <entity name="Payer"> + <data key="firstName">Alex</data> + </entity> </entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml index 8d1b594d44e61..af68a7611cd1d 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml @@ -57,7 +57,7 @@ <element name="email" type="input" selector="//input[contains(@name, 'email') and not(contains(@style, 'display:none'))]"/> <element name="password" type="input" selector="//input[contains(@name, 'password') and not(contains(@style, 'display:none'))]"/> <element name="loginBtn" type="input" selector="button#btnLogin"/> - <element name="reviewUserInfo" type="text" selector="//p[@id='reviewUserInfo' and contains(text(),'Hi, MPI!')]"/> + <element name="reviewUserInfo" type="text" selector="#reviewUserInfo"/> <element name="cartIcon" type="text" selector="#transactionCart"/> <element name="itemName" type="text" selector="//span[@title='{{productName}}']" parameterized="true"/> <element name="PayPalSubmitBtn" type="text" selector="//input[@type='submit']"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminCheckDefaultValueOfPayPalCustomizeButtonTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminCheckDefaultValueOfPayPalCustomizeButtonTest.xml new file mode 100644 index 0000000000000..5c10bc9536fcf --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminCheckDefaultValueOfPayPalCustomizeButtonTest.xml @@ -0,0 +1,45 @@ +<?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="AdminCheckDefaultValueOfPayPalCustomizeButtonTest"> + <annotations> + <features value="Paypal"/> + <stories value="Button Configuration"/> + <title value="Check Default Value Of Paypal Customize Button"/> + <description value="Default value of Paypal Customize Button should be NO"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10904"/> + <group value="paypal"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey= "openPayPalButtonCheckoutPage"/> + <seeElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <seeOptionIsSelected selector="{{ButtonCustomization.customizeDrpDown}}" userInput="No" stepKey="seeNoIsDefaultValue"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify default value--> + <comment userInput="Verify default value" stepKey="commentVerifyDefaultValue1"/> + <seeElement selector="{{ButtonCustomization.label}}" stepKey="seeLabel"/> + <seeElement selector="{{ButtonCustomization.layout}}" stepKey="seeLayout"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize1"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape1"/> + <seeElement selector="{{ButtonCustomization.color}}" stepKey="seeColor"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml index a6e741f0151e0..cfc7d66ba23cf 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml @@ -20,7 +20,7 @@ </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpress"> + <actionGroup ref="SampleConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpress"> <argument name="credentials" value="SamplePaypalExpressConfig"/> </actionGroup> </before> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml b/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml deleted file mode 100644 index 079b46dc1b0cb..0000000000000 --- a/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml +++ /dev/null @@ -1,170 +0,0 @@ -<?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="CheckDefaultValueOfPayPalCustomizeButtonTest"> - <annotations> - <features value="PayPal"/> - <stories value="Button Configuration"/> - <title value="Check Default Value Of PayPal Customize Button"/> - <description value="Default value of PayPal Customize Button should be NO"/> - <severity value="AVERAGE"/> - <testCaseId value="MC-10904"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - </before> - <after> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </after> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> - <seeElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> - <seeOptionIsSelected selector="{{ButtonCustomization.customizeDrpDown}}" userInput="No" stepKey="seeNoIsDefaultValue"/> - <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> - <!--Verify default value--> - <comment userInput="Verify default value" stepKey="commentVerifyDefaultValue1"/> - <seeElement selector="{{ButtonCustomization.label}}" stepKey="seeLabel"/> - <seeElement selector="{{ButtonCustomization.layout}}" stepKey="seeLayout"/> - <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize1"/> - <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape1"/> - <seeElement selector="{{ButtonCustomization.color}}" stepKey="seeColor"/> - </test> - <test name="CheckCreditButtonConfiguration"> - <annotations> - <features value="PayPal"/> - <stories value="Button Configuration"/> - <title value="Check Credit Button Configuration"/> - <description value="Admin is able to customize Credit button"/> - <severity value="AVERAGE"/> - <testCaseId value="MC-10900"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - <createData entity="_defaultProduct" stepKey="createPreReqProduct"> - <requiredEntity createDataKey="createPreReqCategory"/> - </createData> - <!-- Create Customer --> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <!--Config PayPal Express Checkout--> - <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - </before> - <after> - <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> - <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> - <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </after> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <!--Navigate to button configuration setting--> - <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> - <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> - <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> - <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> - <!--Verify Credit Button value--> - <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> - <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> - <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize2"/> - <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape2"/> - <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> - <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> - <!--Customize Credit Button--> - <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> - <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> - <!--Save configuration--> - <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> - <waitForPageLoad stepKey="waitForConfigSave"/> - <openNewTab stepKey="openNewTab"/> - <amOnPage url="/" stepKey="openStorefront"/> - <!--Login to storefront as previously created customer--> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> - <argument name="Customer" value="$$createCustomer$$"/> - </actionGroup> - <actionGroup ref="addProductToCheckoutPage" stepKey="addProductToCheckoutPage"> - <argument name="Category" value="$$createPreReqCategory$$"/> - </actionGroup> - <!--set ID for iframe of PayPal group button--> - <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> - <!--switch to iframe of PayPal group button--> - <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> - <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> - <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> - <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> - <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> - </test> - <test name="PayPalSmartButtonInCheckoutPage"> - <annotations> - <features value="PayPal"/> - <stories value="Generic checkout skeleton flow"/> - <title value="Mainflow of PayPal Smart Button"/> - <description value="Users are able to place order using PayPal Smart Button"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-13690"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - <createData entity="_defaultProduct" stepKey="createPreReqProduct"> - <requiredEntity createDataKey="createPreReqCategory"/> - </createData> - <!-- Create Customer --> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <!--Config PayPal Express Checkout--> - <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - <magentoCLI command="config:set payment/paypal_express/in_context 1" stepKey="disableInContextPayPal"/> - </before> - <after> - <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> - <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> - <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> - <magentoCLI command="config:set payment/paypal_express/in_context 0" stepKey="enableInContextPayPal"/> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </after> - <magentoCLI command="config:set payment/paypal_express/payment_action Authorization" stepKey="inputPaymentAction"/> - <magentoCLI command="config:set payment/paypal_express/solution_type Sole" stepKey="enablePayPalGuestCheckout"/> - <magentoCLI command="config:set payment/paypal_express/line_items_enabled 1" stepKey="enableTransferCartLine"/> - <magentoCLI command="config:set payment/paypal_express/skip_order_review_step 1" stepKey="enableSkipOrderReview"/> - <!--Login to storefront as previously created customer--> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> - <argument name="Customer" value="$$createCustomer$$"/> - </actionGroup> - <!--Place an order using PayPal method--> - <comment userInput="Place an order using PayPal method" stepKey="commentPayPalPlaceOrder"/> - <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> - <argument name="Category" value="$$createPreReqCategory$$"/> - </actionGroup> - <!--Open Cart on PayPal--> - <comment userInput="Open Cart on PayPal" stepKey="commentOpenCart"/> - <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> - <seeElement selector="{{PayPalPaymentSection.itemName($$createPreReqProduct.name$$)}}" stepKey="seeProductname"/> - <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> - <switchToPreviousTab stepKey="switchToPreviousTab"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <!--I see order successful Page instead of Order Review Page--> - <comment userInput="I see order successful Page instead of Order Review Page" stepKey="commentVerifyOrderReviewPage"/> - <waitForElement selector="{{CheckoutSuccessMainSection.successTitle}}" stepKey="waitForLoadSuccessPageTitle"/> - <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> - <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="seeOrderLink"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml new file mode 100644 index 0000000000000..d8cea82bcc2f5 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml @@ -0,0 +1,80 @@ +<?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="StorefrontCheckCreditButtonConfigurationTest"> + <annotations> + <features value="Paypal"/> + <stories value="Button Configuration"/> + <title value="Check Credit Button Configuration"/> + <description value="Admin is able to customize Credit button"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10900"/> + <group value="paypal"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Navigate to button configuration setting--> + <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> + <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify Credit Button value--> + <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> + <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape"/> + <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> + <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> + <!--Customize Credit Button--> + <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> + <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSave"/> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="/" stepKey="openStorefront"/> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addProductToCheckoutPage" stepKey="addProductToCheckoutPage"> + <argument name="Category" value="$$createPreReqCategory$$"/> + </actionGroup> + <!--set ID for iframe of PayPal group button--> + <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> + <!--switch to iframe of PayPal group button--> + <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> + <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml new file mode 100644 index 0000000000000..6adba94e96890 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -0,0 +1,83 @@ +<?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="StorefrontPaypalSmartButtonInCheckoutPageTest"> + <annotations> + <features value="Paypal"/> + <stories value="Generic checkout skeleton flow"/> + <title value="Mainflow of Paypal Smart Button"/> + <description value="Users are able to place order using Paypal Smart Button"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13690"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + <group value="paypal"/> + </annotations> + <before> + + <!-- Create Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Set Paypal express config --> + <magentoCLI command="config:set {{StorefrontPaypalEnableConfigData.path}} {{StorefrontPaypalEnableConfigData.value}}" stepKey="enablePaypal"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableInContextCheckoutConfigData.path}} {{StorefrontPaypalEnableInContextCheckoutConfigData.value}}" stepKey="enableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{_CREDS.magento/paypal_express_checkout_us_merchant_id}}" stepKey="setMerchantId"/> + <createData entity="PaypalConfig" stepKey="createPaypalExpressConfig"/> + + <!-- Login --> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + </before> + <after> + <!-- Cleanup Paypal configurations --> + <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{StorefrontPaypalMerchantAccountIdConfigData.value}}" stepKey="deleteMerchantId"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> + <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> + + <!-- Delete product --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete customer --> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Place an order using PayPal payment method --> + <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> + <argument name="Category" value="$$createCategory$$"/> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!-- PayPal checkout --> + <actionGroup ref="StorefrontPayOrderOnPayPalCheckoutActionGroup" stepKey="payOrderOnPayPalCheckout"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php index e4de60cafb8ad..1bea6b11b966b 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php @@ -6,36 +6,36 @@ namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Fieldset; -/** - * Class GroupTest - */ class GroupTest extends \PHPUnit\Framework\TestCase { /** * @var Group */ - protected $_model; + private $_model; /** * @var \Magento\Framework\Data\Form\Element\AbstractElement */ - protected $_element; + private $_element; /** * @var \Magento\Backend\Model\Auth\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $_authSession; + private $_authSession; /** * @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject */ - protected $_user; + private $_user; /** * @var \Magento\Config\Model\Config\Structure\Element\Group|\PHPUnit_Framework_MockObject_MockObject */ - protected $_group; + private $_group; + /** + * @inheritdoc + */ protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -89,7 +89,7 @@ public function testIsCollapseState($expanded, $expected) $html = $this->_model->render($this->_element); $this->assertContains( '<input id="' . $this->_element->getHtmlId() . '-state" name="config_state[' - . $this->_element->getId() . ']" type="hidden" value="' . $expected . '" />', + . $this->_element->getId() . ']" type="hidden" value="' . $expected . '" />', $html ); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php index 478607f9956e6..7256984ab5226 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -32,7 +32,7 @@ 'label' => 'installment', 'installmentperiod' => 0 ], - 'isVisibleOnProductPage' => 0, + 'isVisibleOnProductPage' => false, 'isGuestCheckoutAllowed' => true ] ], @@ -62,7 +62,7 @@ 'label' => 'installment', 'installmentperiod' => 0 ], - 'isVisibleOnProductPage' => 0, + 'isVisibleOnProductPage' => false, 'isGuestCheckoutAllowed' => true ] ], @@ -91,7 +91,7 @@ 'shape' => 'rect', 'label' => 'paypal' ], - 'isVisibleOnProductPage' => 0, + 'isVisibleOnProductPage' => false, 'isGuestCheckoutAllowed' => true ] ], @@ -120,7 +120,7 @@ 'shape' => 'rect', 'label' => 'paypal' ], - 'isVisibleOnProductPage' => 0, + 'isVisibleOnProductPage' => false, 'isGuestCheckoutAllowed' => true ] ], @@ -149,7 +149,7 @@ 'shape' => 'rect', 'label' => 'paypal', ], - 'isVisibleOnProductPage' => 0, + 'isVisibleOnProductPage' => false, 'isGuestCheckoutAllowed' => true ] ] diff --git a/app/code/Magento/Paypal/etc/adminhtml/system.xml b/app/code/Magento/Paypal/etc/adminhtml/system.xml index ca886c7827ffd..88bb61f2cdc99 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system.xml @@ -13,7 +13,7 @@ <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> <field id="merchant_country" type="select" translate="label comment" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Merchant Country</label> - <comment>If not specified, Default Country from General Config will be used</comment> + <comment>If not specified, Default Country from General Config will be used.</comment> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Country</frontend_model> <source_model>Magento\Paypal\Model\System\Config\Source\MerchantCountry</source_model> <backend_model>Magento\Paypal\Model\System\Config\Backend\MerchantCountry</backend_model> diff --git a/app/code/Magento/Paypal/etc/config.xml b/app/code/Magento/Paypal/etc/config.xml index 4619fc8539442..6c0601f80137d 100644 --- a/app/code/Magento/Paypal/etc/config.xml +++ b/app/code/Magento/Paypal/etc/config.xml @@ -164,7 +164,6 @@ <title>Credit Card (Payflow Advanced) PayPal PayPal - PayPal 1 1 GET diff --git a/app/code/Magento/Paypal/etc/db_schema.xml b/app/code/Magento/Paypal/etc/db_schema.xml index 4dc283fcb48ce..3300f3754e656 100644 --- a/app/code/Magento/Paypal/etc/db_schema.xml +++ b/app/code/Magento/Paypal/etc/db_schema.xml @@ -9,17 +9,17 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + comment="Agreement ID"/> + comment="Customer ID"/> - + + comment="Store ID"/> @@ -40,9 +40,9 @@
+ comment="Agreement ID"/> + comment="Order ID"/> @@ -59,9 +59,9 @@
+ comment="Report ID"/> - + @@ -75,15 +75,15 @@
+ comment="Row ID"/> - - + comment="Report ID"/> + + + comment="Paypal Reference ID"/> + comment="Paypal Reference ID Type"/> - + @@ -117,9 +117,9 @@
+ comment="Cert ID"/> + default="0" comment="Website ID"/> @@ -135,7 +135,7 @@ comment="PayPal Payflow Link Payment Transaction"> - + @@ -146,11 +146,11 @@
- + + comment="Paypal Correlation ID"/>
.admin__collapsible-block > a::before {content: "" !important;} .paypal-other-header > .admin__collapsible-block > a::before {content: '' !important; width: 0; height: 0; border-color: transparent; border-top-color: #000; border-style: solid; border-width: .8rem .5rem 0 .5rem; margin-top:1px; transition: all .2s linear;} .paypal-other-header > .admin__collapsible-block > a.open::before {border-color: transparent; border-bottom-color: #000; border-width: 0 .5rem .8rem .5rem;} -.paypal-other-header > .admin__collapsible-block > a {color: #007bdb !important; text-align: right;} .payments-other-header > .admin__collapsible-block > a, -.paypal-recommended-header > .admin__collapsible-block > a {display: inline-block;} +.paypal-recommended-header > .admin__collapsible-block > a, +.paypal-other-header > .admin__collapsible-block > a {display: inline-block;} .payments-other-header > .admin__collapsible-block > a::before, .paypal-recommended-header > .admin__collapsible-block > a::before {content: '' !important; width: 0; height: 0; border-color: transparent; border-top-color: #000; border-style: solid; border-width: .8rem .5rem 0 .5rem; margin-top:1px; transition: all .2s linear;} .payments-other-header > .admin__collapsible-block > a.open::before, diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml index 7a94ac56232bc..8e222ca7eb04d 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml @@ -136,10 +136,6 @@ value="escapeHtml(__('Place Order')) ?>"> escapeHtml(__('Place Order')) ?> -
+ comment="Session ID"/> + comment="Customer ID"/> diff --git a/app/code/Magento/Persistent/etc/frontend/sections.xml b/app/code/Magento/Persistent/etc/frontend/sections.xml index 16b44c502fc47..7466fbe990b02 100644 --- a/app/code/Magento/Persistent/etc/frontend/sections.xml +++ b/app/code/Magento/Persistent/etc/frontend/sections.xml @@ -10,4 +10,7 @@
+ +
+ diff --git a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml index 0447b3e1b9cef..ba55895453a09 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml @@ -1,4 +1,5 @@
getRandomString(10); ?> - isRememberMeChecked()) : ?> checked="checked" title="escapeHtmlAttr(__('Remember Me')) ?>" /> + isRememberMeChecked()) : ?> checked="checked" title="escapeHtmlAttr(__('Remember Me')) ?>" /> - escapeHtml(__('What\'s this?')) ?> - - escapeHtml(__('Check "Remember Me" to access your shopping cart on this computer even if you are not signed in.')) ?> - + escapeHtml(__('What\'s this?')) ?> + escapeHtml(__('Check "Remember Me" to access your shopping cart on this computer even if you are not signed in.')) ?>
diff --git a/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html b/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html index f5dd1ffd57d9d..427e6bdcd63ab 100644 --- a/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html +++ b/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html @@ -9,8 +9,7 @@ - - - + + diff --git a/app/code/Magento/ProductAlert/Controller/Add/Price.php b/app/code/Magento/ProductAlert/Controller/Add/Price.php index 973db8c3bf5d4..2dbcc27cd57d9 100644 --- a/app/code/Magento/ProductAlert/Controller/Add/Price.php +++ b/app/code/Magento/ProductAlert/Controller/Add/Price.php @@ -6,16 +6,17 @@ namespace Magento\ProductAlert\Controller\Add; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\ProductAlert\Controller\Add as AddController; -use Magento\Framework\App\Action\Context; -use Magento\Customer\Model\Session as CustomerSession; -use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\UrlInterface; +use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\ProductAlert\Controller\Add as AddController; +use Magento\Store\Model\StoreManagerInterface; /** * Controller for notifying about price. @@ -28,15 +29,17 @@ class Price extends AddController implements HttpGetActionInterface protected $storeManager; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var \Magento\Catalog\Api\ProductRepositoryInterface */ protected $productRepository; /** - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * Price constructor. + * + * @param Context $context + * @param CustomerSession $customerSession + * @param StoreManagerInterface $storeManager + * @param ProductRepositoryInterface $productRepository */ public function __construct( Context $context, @@ -54,6 +57,7 @@ public function __construct( * * @param string $url * @return bool + * @throws NoSuchEntityException */ protected function isInternal($url) { @@ -68,13 +72,14 @@ protected function isInternal($url) /** * Method for adding info about product alert price. * - * @return \Magento\Framework\Controller\Result\Redirect + * @return \Magento\Framework\Controller\ResultInterface + * @throws NoSuchEntityException */ public function execute() { $backUrl = $this->getRequest()->getParam(Action::PARAM_NAME_URL_ENCODED); $productId = (int)$this->getRequest()->getParam('product_id'); - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (!$backUrl || !$productId) { $resultRedirect->setPath('/'); @@ -93,9 +98,9 @@ public function execute() ->setWebsiteId($store->getWebsiteId()) ->setStoreId($store->getId()); $model->save(); - $this->messageManager->addSuccess(__('You saved the alert subscription.')); + $this->messageManager->addSuccessMessage(__('You saved the alert subscription.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__('There are not enough parameters.')); + $this->messageManager->addErrorMessage(__('There are not enough parameters.')); if ($this->isInternal($backUrl)) { $resultRedirect->setUrl($backUrl); } else { @@ -103,7 +108,7 @@ public function execute() } return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Controller/Add/Stock.php b/app/code/Magento/ProductAlert/Controller/Add/Stock.php index f36fdf5fb715e..38b22d1efb852 100644 --- a/app/code/Magento/ProductAlert/Controller/Add/Stock.php +++ b/app/code/Magento/ProductAlert/Controller/Add/Stock.php @@ -76,13 +76,13 @@ public function execute() ->setWebsiteId($store->getWebsiteId()) ->setStoreId($store->getId()); $model->save(); - $this->messageManager->addSuccess(__('Alert subscription has been saved.')); + $this->messageManager->addSuccessMessage(__('Alert subscription has been saved.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__('There are not enough parameters.')); + $this->messageManager->addErrorMessage(__('There are not enough parameters.')); $resultRedirect->setUrl($backUrl); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php index 2077b1ff2794b..0aabf5d96e1e7 100644 --- a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php +++ b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php @@ -6,6 +6,7 @@ namespace Magento\ProductAlert\Controller\Unsubscribe; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\ProductAlert\Controller\Unsubscribe as UnsubscribeController; use Magento\Framework\App\Action\Context; use Magento\Customer\Model\Session as CustomerSession; @@ -13,7 +14,10 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\NoSuchEntityException; -class Price extends UnsubscribeController +/** + * Class Price + */ +class Price extends UnsubscribeController implements HttpGetActionInterface { /** * @var \Magento\Catalog\Api\ProductRepositoryInterface @@ -35,6 +39,8 @@ public function __construct( } /** + * Unsubscribe action + * * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() @@ -51,8 +57,13 @@ public function execute() /* @var $product \Magento\Catalog\Model\Product */ $product = $this->productRepository->getById($productId); if (!$product->isVisibleInCatalog()) { - throw new NoSuchEntityException(); + $this->messageManager->addErrorMessage( + __("The product wasn't found. Verify the product and try again.") + ); + $resultRedirect->setPath('customer/account/'); + return $resultRedirect; } + /** @var \Magento\ProductAlert\Model\Price $model */ $model = $this->_objectManager->create(\Magento\ProductAlert\Model\Price::class) ->setCustomerId($this->customerSession->getCustomerId()) @@ -67,13 +78,13 @@ public function execute() $model->delete(); } - $this->messageManager->addSuccess(__('You deleted the alert subscription.')); + $this->messageManager->addSuccessMessage(__('You deleted the alert subscription.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__("The product wasn't found. Verify the product and try again.")); + $this->messageManager->addErrorMessage(__("The product wasn't found. Verify the product and try again.")); $resultRedirect->setPath('customer/account/'); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php index 1b729f988e4a5..f8df6ae6af38d 100644 --- a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php +++ b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php @@ -94,13 +94,13 @@ public function execute() if ($model->getId()) { $model->delete(); } - $this->messageManager->addSuccess(__('You will no longer receive stock alerts for this product.')); + $this->messageManager->addSuccessMessage(__('You will no longer receive stock alerts for this product.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__('The product was not found.')); + $this->messageManager->addErrorMessage(__('The product was not found.')); $resultRedirect->setPath('customer/account/'); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Test/Unit/Controller/Unsubscribe/PriceTest.php b/app/code/Magento/ProductAlert/Test/Unit/Controller/Unsubscribe/PriceTest.php new file mode 100644 index 0000000000000..a07c93337c041 --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Unit/Controller/Unsubscribe/PriceTest.php @@ -0,0 +1,125 @@ +objectManager = new ObjectManager($this); + $this->requestMock = $this->createMock(Http::class); + $this->resultFactoryMock = $this->createMock(ResultFactory::class); + $this->resultRedirectMock = $this->createMock(Redirect::class); + $this->messageManagerMock = $this->createMock(Manager::class); + $this->productMock = $this->createMock(Product::class); + $this->contextMock = $this->createMock(Context::class); + $this->customerSessionMock = $this->createMock(Session::class); + $this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class); + $this->resultFactoryMock->expects($this->any()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); + $this->contextMock->expects($this->any())->method('getResultFactory')->willReturn($this->resultFactoryMock); + $this->contextMock->expects($this->any())->method('getMessageManager')->willReturn($this->messageManagerMock); + + $this->priceController = $this->objectManager->getObject( + Price::class, + [ + 'context' => $this->contextMock, + 'customerSession' => $this->customerSessionMock, + 'productRepository' => $this->productRepositoryMock, + ] + ); + } + + public function testProductIsNotVisibleInCatalog() + { + $productId = 123; + $this->requestMock->expects($this->any())->method('getParam')->with('product')->willReturn($productId); + $this->productRepositoryMock->expects($this->any()) + ->method('getById') + ->with($productId) + ->willReturn($this->productMock); + $this->productMock->expects($this->any())->method('isVisibleInCatalog')->willReturn(false); + $this->messageManagerMock->expects($this->once())->method('addErrorMessage')->with(__("The product wasn't found. Verify the product and try again.")); + $this->resultRedirectMock->expects($this->once())->method('setPath')->with('customer/account/'); + + $this->assertEquals( + $this->resultRedirectMock, + $this->priceController->execute() + ); + } +} diff --git a/app/code/Magento/ProductAlert/Test/Unit/Helper/DataTest.php b/app/code/Magento/ProductAlert/Test/Unit/Helper/DataTest.php new file mode 100644 index 0000000000000..122d785e79599 --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Unit/Helper/DataTest.php @@ -0,0 +1,162 @@ +contextMock = $this->createMock(Context::class); + $this->urlBuilderMock = $this->createMock(UrlInterface::class); + $this->encoderMock = $this->createMock(EncoderInterface::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->layoutMock = $this->createMock(LayoutInterface::class); + $this->contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); + $this->contextMock->expects($this->once())->method('getUrlEncoder')->willReturn($this->encoderMock); + $this->contextMock->expects($this->once())->method('getScopeConfig')->willReturn($this->scopeConfigMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $productMock = $this->createMock(Product::class); + $productMock->expects($this->any())->method('getId')->willReturn(1); + + $this->helper = $this->objectManagerHelper->getObject( + HelperData::class, + [ + 'context' => $this->contextMock, + 'layout' => $this->layoutMock + ] + ); + $this->helper->setProduct($productMock); + } + + /** + * Test getSaveUrl() function + */ + public function testGetSaveUrl() + { + $currentUrl = 'http://www.example.com/'; + $type = 'stock'; + $uenc = strtr(base64_encode($currentUrl), '+/=', '-_,'); + $expected = 'http://www.example.com/roductalert/add/stock/product_id/1/uenc/' . $uenc; + + $this->urlBuilderMock->expects($this->any())->method('getCurrentUrl')->willReturn($currentUrl); + $this->encoderMock->expects($this->any())->method('encode') + ->with($currentUrl) + ->willReturn($uenc); + $this->urlBuilderMock->expects($this->any())->method('getUrl') + ->with( + 'productalert/add/' . $type, + [ + 'product_id' => 1, + 'uenc' => $uenc + ] + ) + ->willReturn($expected); + + $this->assertEquals($expected, $this->helper->getSaveUrl($type)); + } + + /** + * Test createBlock() with no exception + */ + public function testCreateBlockWithNoException() + { + $priceBlockMock = $this->createMock(Price::class); + $this->layoutMock->expects($this->once())->method('createBlock')->willReturn($priceBlockMock); + + $this->assertEquals($priceBlockMock, $this->helper->createBlock(Price::class)); + } + + /** + * Test createBlock() with exception + */ + public function testCreateBlockWithException() + { + $invalidBlock = $this->createMock(Product::class); + $this->expectException(LocalizedException::class); + + $this->helper->createBlock($invalidBlock); + } + + /** + * Test isStockAlertAllowed() function with Yes settings + */ + public function testIsStockAlertAllowedWithYesSettings() + { + $this->scopeConfigMock->expects($this->any())->method('isSetFlag') + ->with(Observer::XML_PATH_STOCK_ALLOW, ScopeInterface::SCOPE_STORE) + ->willReturn('1'); + + $this->assertEquals('1', $this->helper->isStockAlertAllowed()); + } + + /** + * Test isPriceAlertAllowed() function with Yes settings + */ + public function testIsPriceAlertAllowedWithYesSetting() + { + $this->scopeConfigMock->expects($this->any())->method('isSetFlag') + ->with(Observer::XML_PATH_PRICE_ALLOW, ScopeInterface::SCOPE_STORE) + ->willReturn('1'); + + $this->assertEquals('1', $this->helper->isPriceAlertAllowed()); + } +} diff --git a/app/code/Magento/ProductAlert/etc/db_schema.xml b/app/code/Magento/ProductAlert/etc/db_schema.xml index cb91560f8daa6..17cc76246e5c6 100644 --- a/app/code/Magento/ProductAlert/etc/db_schema.xml +++ b/app/code/Magento/ProductAlert/etc/db_schema.xml @@ -9,17 +9,17 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Product alert price ID"/> + default="0" comment="Customer ID"/> + default="0" comment="Product ID"/> + default="0" comment="Website ID"/> + default="0" comment="Store ID"/>
+ comment="Product alert stock ID"/> + default="0" comment="Customer ID"/> + default="0" comment="Product ID"/> + default="0" comment="Website ID"/> + default="0" comment="Store ID"/> + + + + + + + + + + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminFillProductVideoFieldActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminFillProductVideoFieldActionGroup.xml new file mode 100644 index 0000000000000..c4448dee84cd5 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminFillProductVideoFieldActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminGetVideoInformationActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminGetVideoInformationActionGroup.xml new file mode 100644 index 0000000000000..c786a77de97f2 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminGetVideoInformationActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminOpenProductVideoModalActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminOpenProductVideoModalActionGroup.xml new file mode 100644 index 0000000000000..569200efa7379 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminOpenProductVideoModalActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AssertAdminVideoValidationErrorActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AssertAdminVideoValidationErrorActionGroup.xml new file mode 100644 index 0000000000000..60e2391712f1b --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AssertAdminVideoValidationErrorActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml index 71dad6a24f148..58a1c40a4e470 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml @@ -12,6 +12,7 @@ + @@ -20,5 +21,6 @@ + \ No newline at end of file diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminValidateUrlOnGetVideoInformationTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminValidateUrlOnGetVideoInformationTest.xml new file mode 100644 index 0000000000000..0db15276e8c67 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminValidateUrlOnGetVideoInformationTest.xml @@ -0,0 +1,45 @@ + + + + + + + + + <description value="Testing for a required video url when getting video information"/> + <severity value="CRITICAL"/> + <group value="ProductVideo"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setStoreConfig"/> + </before> + <after> + <createData entity="DefaultProductVideoConfig" stepKey="setStoreDefaultConfig"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="AdminOpenProductVideoModalActionGroup" stepKey="openAddProductVideoModal"/> + <actionGroup ref="AdminGetVideoInformationActionGroup" stepKey="clickOnGetVideoInformation"/> + <actionGroup ref="AssertAdminVideoValidationErrorActionGroup" stepKey="seeUrlValidationMessage"> + <argument name="inputName" value="video_url"/> + </actionGroup> + <actionGroup ref="AdminFillProductVideoFieldActionGroup" stepKey="fillVideoUrlField"> + <argument name="input" value="{{AdminProductNewVideoSection.videoUrlTextField}}"/> + <argument name="value" value="{{mftfTestProductVideo.videoUrl}}"/> + </actionGroup> + <actionGroup ref="AdminGetVideoInformationActionGroup" stepKey="clickOnGetVideoInformation2"/> + <actionGroup ref="AdminAssertVideoNoValidationErrorActionGroup" stepKey="dontSeeUrlValidationMessage"> + <argument name="inputName" value="video_url"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js index e9b234c5f1160..9cc731dde4b0c 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js @@ -282,9 +282,15 @@ define([ * @private */ _onGetVideoInformationClick: function () { - this._onlyVideoPlayer = false; - this._isEditPage = false; - this._videoUrlWidget.trigger('update_video_information'); + var videoForm = this.element.find(this._videoFormSelector); + + videoForm.validation(); + + if (this.element.find(this._videoUrlSelector).valid()) { + this._onlyVideoPlayer = false; + this._isEditPage = false; + this._videoUrlWidget.trigger('update_video_information'); + } }, /** @@ -299,6 +305,14 @@ define([ * @private */ _onGetVideoInformationStartRequest: function () { + var videoForm = this.element.find(this._videoFormSelector); + + try { + videoForm.validation('clearError'); + } catch (e) { + // Do nothing + } + this._videoRequestComplete = false; }, diff --git a/app/code/Magento/Quote/Api/CartManagementInterface.php b/app/code/Magento/Quote/Api/CartManagementInterface.php index 7aa4bc4c7603a..dc8ab7fedc870 100644 --- a/app/code/Magento/Quote/Api/CartManagementInterface.php +++ b/app/code/Magento/Quote/Api/CartManagementInterface.php @@ -52,6 +52,9 @@ public function getCartForCustomer($customerId); * @param int $customerId The customer ID. * @param int $storeId * @return boolean + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function assignCustomer($cartId, $customerId, $storeId); diff --git a/app/code/Magento/Quote/Api/Data/CartSearchResultsInterface.php b/app/code/Magento/Quote/Api/Data/CartSearchResultsInterface.php index 5da776091f7f1..48818e5d253e6 100644 --- a/app/code/Magento/Quote/Api/Data/CartSearchResultsInterface.php +++ b/app/code/Magento/Quote/Api/Data/CartSearchResultsInterface.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Api\Data; /** diff --git a/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php b/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php index 266a6e67528e4..678c92250f531 100644 --- a/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php +++ b/app/code/Magento/Quote/Model/Cart/Totals/ItemConverter.php @@ -71,7 +71,7 @@ public function __construct( * Converts a specified rate model to a shipping method data object. * * @param \Magento\Quote\Model\Quote\Item $item - * @return array + * @return \Magento\Quote\Api\Data\TotalsItemInterface * @throws \Exception */ public function modelToDataObject($item) diff --git a/app/code/Magento/Quote/Model/CartSearchResults.php b/app/code/Magento/Quote/Model/CartSearchResults.php new file mode 100644 index 0000000000000..5c505bec121f2 --- /dev/null +++ b/app/code/Magento/Quote/Model/CartSearchResults.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Framework\Api\AbstractSimpleObject; +use Magento\Quote\Api\Data\CartSearchResultsInterface; + +/** + * Service Data Object with Cart search results. + */ +class CartSearchResults extends AbstractSimpleObject implements CartSearchResultsInterface +{ + /** + * @inheritdoc + */ + public function setItems(array $items = null) + { + return $this->setData(self::KEY_ITEMS, $items); + } + + /** + * @inheritdoc + */ + public function getItems() + { + return $this->_get(self::KEY_ITEMS) === null ? [] : $this->_get(self::KEY_ITEMS); + } + + /** + * @inheritdoc + */ + public function getSearchCriteria() + { + return $this->_get(self::KEY_SEARCH_CRITERIA); + } + + /** + * @inheritdoc + */ + public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) + { + return $this->setData(self::KEY_SEARCH_CRITERIA, $searchCriteria); + } + + /** + * @inheritdoc + */ + public function getTotalCount() + { + return $this->_get(self::KEY_TOTAL_COUNT); + } + + /** + * @inheritdoc + */ + public function setTotalCount($count) + { + return $this->setData(self::KEY_TOTAL_COUNT, $count); + } +} diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 3ecbc69b80785..0e2bf1ce48ec7 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -425,9 +425,11 @@ protected function _populateBeforeSaveData() */ protected function _isSameAsBilling() { + $quoteSameAsBilling = $this->getSameAsBilling(); + return $this->getAddressType() == \Magento\Quote\Model\Quote\Address::TYPE_SHIPPING && - ($this->_isNotRegisteredCustomer() || - $this->_isDefaultShippingNullOrSameAsBillingAddress()); + ($this->_isNotRegisteredCustomer() || $this->_isDefaultShippingNullOrSameAsBillingAddress()) && + ($quoteSameAsBilling || $quoteSameAsBilling === 0 || $quoteSameAsBilling === null); } /** @@ -471,7 +473,7 @@ protected function _isDefaultShippingNullOrSameAsBillingAddress() /** * Declare address quote model object * - * @param \Magento\Quote\Model\Quote $quote + * @param \Magento\Quote\Model\Quote $quote * @return $this */ public function setQuote(\Magento\Quote\Model\Quote $quote) @@ -691,7 +693,7 @@ public function getItemQty($itemId = 0) */ public function hasItems() { - return sizeof($this->getAllItems()) > 0; + return count($this->getAllItems()) > 0; } /** @@ -1225,8 +1227,8 @@ public function setBaseShippingAmount($value, $alreadyExclTax = false) /** * Set total amount value * - * @param string $code - * @param float $amount + * @param string $code + * @param float $amount * @return $this */ public function setTotalAmount($code, $amount) @@ -1243,8 +1245,8 @@ public function setTotalAmount($code, $amount) /** * Set total amount value in base store currency * - * @param string $code - * @param float $amount + * @param string $code + * @param float $amount * @return $this */ public function setBaseTotalAmount($code, $amount) @@ -1261,8 +1263,8 @@ public function setBaseTotalAmount($code, $amount) /** * Add amount total amount value * - * @param string $code - * @param float $amount + * @param string $code + * @param float $amount * @return $this */ public function addTotalAmount($code, $amount) @@ -1276,8 +1278,8 @@ public function addTotalAmount($code, $amount) /** * Add amount total amount value in base store currency * - * @param string $code - * @param float $amount + * @param string $code + * @param float $amount * @return $this */ public function addBaseTotalAmount($code, $amount) diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php index 00060c15c10d8..d8dd0953407d4 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php @@ -168,7 +168,7 @@ public function getAllBaseTotalAmounts() { return $this->baseTotalAmounts; } - + //@codeCoverageIgnoreEnd /** diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Grand.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Grand.php index 13f41f909d43f..2cefec2c9035a 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Grand.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Grand.php @@ -3,43 +3,64 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model\Quote\Address\Total; -class Grand extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\PriceCurrencyInterface as PriceRounder; +use Magento\Quote\Api\Data\ShippingAssignmentInterface as ShippingAssignment; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; + +/** + * Collect grand totals. + */ +class Grand extends AbstractTotal { + /** + * @var PriceRounder + */ + private $priceRounder; + + /** + * @param PriceRounder|null $priceRounder + */ + public function __construct(?PriceRounder $priceRounder) + { + $this->priceRounder = $priceRounder?: ObjectManager::getInstance()->get(PriceRounder::class); + } + /** * Collect grand total address amount * - * @param \Magento\Quote\Model\Quote $quote - * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Quote\Model\Quote\Address\Total $total - * @return $this + * @param Quote $quote + * @param ShippingAssignment $shippingAssignment + * @param Total $total + * @return Grand * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function collect( - \Magento\Quote\Model\Quote $quote, - \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment, - \Magento\Quote\Model\Quote\Address\Total $total - ) { - $grandTotal = $total->getGrandTotal(); - $baseGrandTotal = $total->getBaseGrandTotal(); + public function collect(Quote $quote, ShippingAssignment $shippingAssignment, Total $total): Grand + { $totals = array_sum($total->getAllTotalAmounts()); $baseTotals = array_sum($total->getAllBaseTotalAmounts()); + $grandTotal = $this->priceRounder->roundPrice($total->getGrandTotal() + $totals, 4); + $baseGrandTotal = $this->priceRounder->roundPrice($total->getBaseGrandTotal() + $baseTotals, 4); - $total->setGrandTotal($grandTotal + $totals); - $total->setBaseGrandTotal($baseGrandTotal + $baseTotals); + $total->setGrandTotal($grandTotal); + $total->setBaseGrandTotal($baseGrandTotal); return $this; } /** * Add grand total information to address * - * @param \Magento\Quote\Model\Quote $quote - * @param \Magento\Quote\Model\Quote\Address\Total $total - * @return $this + * @param Quote $quote + * @param Total $total + * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total) + public function fetch(Quote $quote, Total $total): array { return [ 'code' => $this->getCode(), diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 84ef699b6209e..3a81341e2b02a 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -298,22 +298,28 @@ public function assignCustomer($cartId, $customerId, $storeId) ); } try { - $this->quoteRepository->getForCustomer($customerId); - throw new StateException( - __("The customer can't be assigned to the cart because the customer already has an active cart.") - ); + $customerActiveQuote = $this->quoteRepository->getForCustomer($customerId); + + $quote->merge($customerActiveQuote); + $customerActiveQuote->setIsActive(0); + $this->quoteRepository->save($customerActiveQuote); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } $quote->setCustomer($customer); $quote->setCustomerIsGuest(0); + $quote->setIsActive(1); + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'quote_id'); if ($quoteIdMask->getId()) { $quoteIdMask->delete(); } + $this->quoteRepository->save($quote); + return true; } diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index 30931821ddc7d..ccfd3df5fafa3 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -224,7 +224,7 @@ protected function loadQuote($loadMethod, $loadField, $identifier, array $shared { /** @var CartInterface $quote */ $quote = $this->cartFactory->create(); - if ($sharedStoreIds && method_exists($quote, 'setSharedStoreIds')) { + if ($sharedStoreIds && is_callable([$quote, 'setSharedStoreIds'])) { $quote->setSharedStoreIds($sharedStoreIds); } $quote->setStoreId($this->storeManager->getStore()->getId())->$loadMethod($identifier); diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index 392a815ed963c..443e4fda1bd8c 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\ResourceModel\Quote\Item as ResourceQuoteItem; @@ -256,8 +257,17 @@ protected function _assignProducts(): self foreach ($this as $item) { /** @var ProductInterface $product */ $product = $productCollection->getItemById($item->getProductId()); + try { + /** @var QuoteItem $item */ + $parentItem = $item->getParentItem(); + $parentProduct = $parentItem ? $parentItem->getProduct() : null; + } catch (NoSuchEntityException $exception) { + $parentItem = null; + $parentProduct = null; + $this->_logger->error($exception); + } $qtyOptions = []; - if ($product && $this->isValidProduct($product)) { + if ($this->isValidProduct($product) && (!$parentItem || $this->isValidProduct($parentProduct))) { $product->setCustomOptions([]); $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection); foreach ($optionProductIds as $optionProductId) { @@ -276,7 +286,7 @@ protected function _assignProducts(): self } } if ($this->recollectQuote && $this->_quote) { - $this->_quote->collectTotals(); + $this->_quote->setTotalsCollectedFlag(false); } \Magento\Framework\Profiler::stop('QUOTE:' . __METHOD__); @@ -327,7 +337,7 @@ private function getOptionProductIds( * @param ProductInterface $product * @return bool */ - private function isValidProduct(ProductInterface $product): bool + private function isValidProduct(?ProductInterface $product): bool { $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED); diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index 77dfec9603a5c..a1228903e2323 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -113,6 +113,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) $customerCountryCode = $customerAddress->getCountryId(); $customerVatNumber = $customerAddress->getVatId(); + $address->setCountryId($customerCountryCode); + $address->setVatId($customerVatNumber); } $groupId = null; diff --git a/app/code/Magento/Quote/Observer/SubmitObserver.php b/app/code/Magento/Quote/Observer/SubmitObserver.php index 1213636e5966b..db0ba7fb77937 100644 --- a/app/code/Magento/Quote/Observer/SubmitObserver.php +++ b/app/code/Magento/Quote/Observer/SubmitObserver.php @@ -5,13 +5,21 @@ */ namespace Magento\Quote\Observer; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Model\Quote; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Psr\Log\LoggerInterface; +/** + * Class responsive for sending order and invoice emails when it's created through storefront. + */ class SubmitObserver implements ObserverInterface { /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ private $logger; @@ -21,27 +29,37 @@ class SubmitObserver implements ObserverInterface private $orderSender; /** - * @param \Psr\Log\LoggerInterface $logger + * @var InvoiceSender + */ + private $invoiceSender; + + /** + * @param LoggerInterface $logger * @param OrderSender $orderSender + * @param InvoiceSender $invoiceSender */ public function __construct( - \Psr\Log\LoggerInterface $logger, - OrderSender $orderSender + LoggerInterface $logger, + OrderSender $orderSender, + InvoiceSender $invoiceSender ) { $this->logger = $logger; $this->orderSender = $orderSender; + $this->invoiceSender = $invoiceSender; } /** - * @param \Magento\Framework\Event\Observer $observer + * Send order and invoice email. + * + * @param Observer $observer * * @return void */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { - /** @var \Magento\Quote\Model\Quote $quote */ + /** @var Quote $quote */ $quote = $observer->getEvent()->getQuote(); - /** @var \Magento\Sales\Model\Order $order */ + /** @var Order $order */ $order = $observer->getEvent()->getOrder(); /** @@ -51,6 +69,10 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (!$redirectUrl && $order->getCanSendNewEmailFlag()) { try { $this->orderSender->send($order); + $invoice = current($order->getInvoiceCollection()->getItems()); + if ($invoice) { + $this->invoiceSender->send($invoice); + } } catch (\Exception $e) { $this->logger->critical($e); } diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 4ec608a18a686..f7a4fda4f67d8 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -73,6 +73,9 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> + <magentoCLI command="config:set customer/online_customers/section_data_lifetime 1" + stepKey="setConfigForCartLifetime"/> + <magentoCLI command="cache:flush" stepKey="flushCache" /> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> @@ -116,6 +119,7 @@ </actionGroup> <closeTab stepKey="closeTab"/> <!-- Check cart --> + <wait time="60" stepKey="waitForCartToBeUpdated"/> <reloadPage stepKey="reloadPage"/> <waitForPageLoad stepKey="waitForCheckoutPageReload"/> <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart"/> @@ -143,6 +147,7 @@ </actionGroup> <closeTab stepKey="closeTab2"/> <!--Check cart--> + <wait time="60" stepKey="waitForCartToBeUpdated2"/> <reloadPage stepKey="reloadPage2"/> <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart2"/> diff --git a/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php b/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php new file mode 100644 index 0000000000000..dc62c57c84941 --- /dev/null +++ b/app/code/Magento/Quote/Test/Unit/Model/ChangeQuoteControlTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Quote\Model\ChangeQuoteControl; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Unit test for \Magento\Quote\Model\ChangeQuoteControl + * + * Class \Magento\Quote\Test\Unit\Model\ChangeQuoteControlTest + */ +class ChangeQuoteControlTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @var \Magento\Quote\Model\ChangeQuoteControl + */ + protected $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $userContextMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $quoteMock; + + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->userContextMock = $this->createMock(UserContextInterface::class); + + $this->model = $this->objectManager->getObject( + ChangeQuoteControl::class, + [ + 'userContext' => $this->userContextMock + ] + ); + + $this->quoteMock = $this->getMockForAbstractClass( + CartInterface::class, + [], + '', + false, + true, + true, + ['getCustomerId'] + ); + } + + /** + * Test if the quote is belonged to customer + */ + public function testIsAllowedIfTheQuoteIsBelongedToCustomer() + { + $quoteCustomerId = 1; + $this->quoteMock->expects($this->any())->method('getCustomerId') + ->will($this->returnValue($quoteCustomerId)); + $this->userContextMock->expects($this->any())->method('getUserType') + ->will($this->returnValue(UserContextInterface::USER_TYPE_CUSTOMER)); + $this->userContextMock->expects($this->any())->method('getUserId') + ->will($this->returnValue($quoteCustomerId)); + + $this->assertEquals(true, $this->model->isAllowed($this->quoteMock)); + } + + /** + * Test if the quote is not belonged to customer + */ + public function testIsAllowedIfTheQuoteIsNotBelongedToCustomer() + { + $currentCustomerId = 1; + $quoteCustomerId = 2; + + $this->quoteMock->expects($this->any())->method('getCustomerId') + ->will($this->returnValue($quoteCustomerId)); + $this->userContextMock->expects($this->any())->method('getUserType') + ->will($this->returnValue(UserContextInterface::USER_TYPE_CUSTOMER)); + $this->userContextMock->expects($this->any())->method('getUserId') + ->will($this->returnValue($currentCustomerId)); + + $this->assertEquals(false, $this->model->isAllowed($this->quoteMock)); + } + + /** + * Test if the quote is belonged to guest and the context is guest + */ + public function testIsAllowedIfQuoteIsBelongedToGuestAndContextIsGuest() + { + $quoteCustomerId = null; + $this->quoteMock->expects($this->any())->method('getCustomerId') + ->will($this->returnValue($quoteCustomerId)); + $this->userContextMock->expects($this->any())->method('getUserType') + ->will($this->returnValue(UserContextInterface::USER_TYPE_GUEST)); + $this->assertEquals(true, $this->model->isAllowed($this->quoteMock)); + } + + /** + * Test if the quote is belonged to customer and the context is guest + */ + public function testIsAllowedIfQuoteIsBelongedToCustomerAndContextIsGuest() + { + $quoteCustomerId = 1; + $this->quoteMock->expects($this->any())->method('getCustomerId') + ->will($this->returnValue($quoteCustomerId)); + $this->userContextMock->expects($this->any())->method('getUserType') + ->will($this->returnValue(UserContextInterface::USER_TYPE_GUEST)); + $this->assertEquals(false, $this->model->isAllowed($this->quoteMock)); + } + + /** + * Test if the context is admin + */ + public function testIsAllowedIfContextIsAdmin() + { + $this->userContextMock->expects($this->any())->method('getUserType') + ->will($this->returnValue(UserContextInterface::USER_TYPE_ADMIN)); + $this->assertEquals(true, $this->model->isAllowed($this->quoteMock)); + } + + /** + * Test if the context is integration + */ + public function testIsAllowedIfContextIsIntegration() + { + $this->userContextMock->expects($this->any())->method('getUserType') + ->will($this->returnValue(UserContextInterface::USER_TYPE_INTEGRATION)); + $this->assertEquals(true, $this->model->isAllowed($this->quoteMock)); + } +} diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/GrandTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/GrandTest.php index 34c51f294c05f..6771583b5bbb0 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/GrandTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/GrandTest.php @@ -6,18 +6,43 @@ namespace Magento\Quote\Test\Unit\Model\Quote\Address\Total; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\Quote\Address\Total\Grand; +use Magento\Framework\Pricing\PriceCurrencyInterface as PriceRounder; +use PHPUnit_Framework_MockObject_MockObject as ObjectMock; +use PHPUnit\Framework\TestCase; -class GrandTest extends \PHPUnit\Framework\TestCase +/** + * Grand totals collector test. + */ +class GrandTest extends TestCase { /** - * @var \Magento\Quote\Model\Quote\Address\Total\Grand + * @var PriceRounder|ObjectMock + */ + private $priceRounder; + + /** + * @var Grand */ - protected $model; + private $model; + /** + * @inheritDoc + */ protected function setUp() { - $objectManager = new ObjectManager($this); - $this->model = $objectManager->getObject(\Magento\Quote\Model\Quote\Address\Total\Grand::class); + $this->priceRounder = $this->getMockBuilder(PriceRounder::class) + ->disableOriginalConstructor() + ->setMethods(['roundPrice']) + ->getMockForAbstractClass(); + + $helper = new ObjectManager($this); + $this->model = $helper->getObject( + Grand::class, + [ + 'priceRounder' => $this->priceRounder, + ] + ); } public function testCollect() @@ -27,14 +52,20 @@ public function testCollect() $grandTotal = 6.4; // 1 + 2 + 3.4 $grandTotalBase = 15.7; // 4 + 5 + 6.7 - $totalMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Total::class, [ + $this->priceRounder->expects($this->at(0))->method('roundPrice')->willReturn($grandTotal + 2); + $this->priceRounder->expects($this->at(1))->method('roundPrice')->willReturn($grandTotalBase + 2); + + $totalMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address\Total::class, + [ 'getAllTotalAmounts', 'getAllBaseTotalAmounts', 'setGrandTotal', 'setBaseGrandTotal', 'getGrandTotal', 'getBaseGrandTotal' - ]); + ] + ); $totalMock->expects($this->once())->method('getGrandTotal')->willReturn(2); $totalMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(2); $totalMock->expects($this->once())->method('getAllTotalAmounts')->willReturn($totals); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index cd2afc39733f2..2c61c192ead62 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -7,11 +7,13 @@ namespace Magento\Quote\Test\Unit\Model; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; use Magento\Framework\App\RequestInterface; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Quote\Model\CustomerManagement; +use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Sales\Api\Data\OrderAddressInterface; @@ -199,7 +201,7 @@ protected function setUp() ); $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, [ 'assignCustomer', 'collectTotals', @@ -275,7 +277,7 @@ public function testCreateEmptyCartAnonymous() $storeId = 345; $quoteId = 2311; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $quoteAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, ['setCollectShippingRates'] @@ -306,14 +308,14 @@ public function testCreateEmptyCartForCustomer() $quoteId = 2311; $userId = 567; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $this->quoteRepositoryMock ->expects($this->once()) ->method('getActiveForCustomer') ->with($userId) - ->willThrowException(new NoSuchEntityException()); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + $customer = $this->getMockBuilder(CustomerInterface::class) ->setMethods(['getDefaultBilling'])->disableOriginalConstructor()->getMockForAbstractClass(); $quoteAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, @@ -342,14 +344,14 @@ public function testCreateEmptyCartForCustomerReturnExistsQuote() $storeId = 345; $userId = 567; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $this->quoteRepositoryMock ->expects($this->once()) ->method('getActiveForCustomer') ->with($userId)->willReturn($quoteMock); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + $customer = $this->getMockBuilder(CustomerInterface::class) ->setMethods(['getDefaultBilling'])->disableOriginalConstructor()->getMockForAbstractClass(); $quoteAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, @@ -379,8 +381,8 @@ public function testAssignCustomerFromAnotherStore() $customerId = 455; $storeId = 5; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $quoteMock = $this->createMock(Quote::class); + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) @@ -395,7 +397,7 @@ public function testAssignCustomerFromAnotherStore() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); @@ -424,10 +426,10 @@ public function testAssignCustomerToNonanonymousCart() $storeId = 5; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getCustomerId', 'setCustomer', 'setCustomerIsGuest'] ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) @@ -442,7 +444,7 @@ public function testAssignCustomerToNonanonymousCart() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); @@ -463,7 +465,7 @@ public function testAssignCustomerToNonanonymousCart() } /** - * @expectedException \Magento\Framework\Exception\StateException + * @expectedException \Magento\Framework\Exception\NoSuchEntityException */ public function testAssignCustomerNoSuchCustomer() { @@ -472,10 +474,51 @@ public function testAssignCustomerNoSuchCustomer() $storeId = 5; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getCustomerId', 'setCustomer', 'setCustomerIsGuest'] ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + + $this->quoteRepositoryMock + ->expects($this->once()) + ->method('getActive') + ->with($cartId) + ->willReturn($quoteMock); + + $this->customerRepositoryMock + ->expects($this->once()) + ->method('getById') + ->with($customerId) + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + + $this->expectExceptionMessage( + "No such entity." + ); + + $this->model->assignCustomer($cartId, $customerId, $storeId); + } + + public function testAssignCustomerWithActiveCart() + { + $cartId = 220; + $customerId = 455; + $storeId = 5; + + $this->getPropertyValue($this->model, 'quoteIdMaskFactory') + ->expects($this->once()) + ->method('create') + ->willReturn($this->quoteIdMock); + + $quoteMock = $this->createPartialMock( + Quote::class, + ['getCustomerId', 'setCustomer', 'setCustomerIsGuest', 'setIsActive', 'getIsActive', 'merge'] + ); + + $activeQuoteMock = $this->createPartialMock( + Quote::class, + ['getCustomerId', 'setCustomer', 'setCustomerIsGuest', 'setIsActive', 'getIsActive', 'merge'] + ); + + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) @@ -490,7 +533,7 @@ public function testAssignCustomerNoSuchCustomer() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); @@ -506,17 +549,26 @@ public function testAssignCustomerNoSuchCustomer() ->willReturn([$storeId, 'some store value']); $quoteMock->expects($this->once())->method('getCustomerId')->willReturn(null); - $this->quoteRepositoryMock ->expects($this->once()) ->method('getForCustomer') - ->with($customerId); + ->with($customerId) + ->willReturn($activeQuoteMock); - $this->model->assignCustomer($cartId, $customerId, $storeId); + $quoteMock->expects($this->once())->method('merge')->with($activeQuoteMock)->willReturnSelf(); + $activeQuoteMock->expects($this->once())->method('setIsActive')->with(0); + $this->quoteRepositoryMock->expects($this->atLeastOnce())->method('save')->with($activeQuoteMock); - $this->expectExceptionMessage( - "The customer can't be assigned to the cart because the customer already has an active cart." - ); + $quoteMock->expects($this->once())->method('setCustomer')->with($customerMock); + $quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(0); + $quoteMock->expects($this->once())->method('setIsActive')->with(1); + + $this->quoteIdMock->expects($this->once())->method('load')->with($cartId, 'quote_id')->willReturnSelf(); + $this->quoteIdMock->expects($this->once())->method('getId')->willReturn(10); + $this->quoteIdMock->expects($this->once())->method('delete'); + $this->quoteRepositoryMock->expects($this->atLeastOnce())->method('save')->with($quoteMock); + + $this->model->assignCustomer($cartId, $customerId, $storeId); } public function testAssignCustomer() @@ -529,15 +581,13 @@ public function testAssignCustomer() ->expects($this->once()) ->method('create') ->willReturn($this->quoteIdMock); - $this->quoteIdMock->expects($this->once())->method('load')->with($cartId, 'quote_id')->willReturnSelf(); - $this->quoteIdMock->expects($this->once())->method('getId')->willReturn(10); - $this->quoteIdMock->expects($this->once())->method('delete'); + $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getCustomerId', 'setCustomer', 'setCustomerIsGuest'] + Quote::class, + ['getCustomerId', 'setCustomer', 'setCustomerIsGuest', 'setIsActive', 'getIsActive', 'merge'] ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) ->method('getActive') @@ -551,10 +601,12 @@ public function testAssignCustomer() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); + $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); + $customerModelMock ->expects($this->once()) ->method('load') @@ -572,11 +624,17 @@ public function testAssignCustomer() ->expects($this->once()) ->method('getForCustomer') ->with($customerId) - ->willThrowException(new NoSuchEntityException()); + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + + $quoteMock->expects($this->never())->method('merge'); $quoteMock->expects($this->once())->method('setCustomer')->with($customerMock); $quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(0); + $quoteMock->expects($this->once())->method('setIsActive')->with(1); + $this->quoteIdMock->expects($this->once())->method('load')->with($cartId, 'quote_id')->willReturnSelf(); + $this->quoteIdMock->expects($this->once())->method('getId')->willReturn(10); + $this->quoteIdMock->expects($this->once())->method('delete'); $this->quoteRepositoryMock->expects($this->once())->method('save')->with($quoteMock); $this->model->assignCustomer($cartId, $customerId, $storeId); @@ -881,7 +939,7 @@ protected function getQuote( \Magento\Quote\Model\Quote\Address $shippingAddress = null ) { $quote = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, [ 'setIsActive', 'getCustomerEmail', @@ -928,7 +986,7 @@ protected function getQuote( ->willReturn($payment); $customer = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['getDefaultBilling', 'getId'] ); $quote->expects($this->any())->method('getCustomerId')->willReturn($customerId); @@ -1016,12 +1074,12 @@ protected function prepareOrderFactory( } /** - * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testGetCartForCustomer() { $customerId = 100; - $cartMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $cartMock = $this->createMock(Quote::class); $this->quoteRepositoryMock->expects($this->once()) ->method('getActiveForCustomer') ->with($customerId) diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php index 095e1760df86f..9c28a06fe83eb 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use PHPUnit\Framework\MockObject\Matcher\InvokedCount as InvokedCountMatch; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Api\Data\CartInterface; @@ -284,7 +285,14 @@ public function testGetWithSharedStoreIds() $this->assertEquals($this->quoteMock, $this->model->get($cartId, $sharedStoreIds)); } - public function testGetForCustomer() + /** + * Test getForCustomer method + * + * @param InvokedCountMatch $invokeTimes + * @param array $sharedStoreIds + * @dataProvider getForCustomerDataProvider + */ + public function testGetForCustomer(InvokedCountMatch $invokeTimes, array $sharedStoreIds) { $cartId = 17; $customerId = 23; @@ -298,7 +306,7 @@ public function testGetForCustomer() $this->storeMock->expects(static::once()) ->method('getId') ->willReturn(1); - $this->quoteMock->expects(static::never()) + $this->quoteMock->expects($invokeTimes) ->method('setSharedStoreIds'); $this->quoteMock->expects(static::once()) ->method('loadByCustomer') @@ -312,8 +320,27 @@ public function testGetForCustomer() ->method('load') ->with($this->quoteMock); + static::assertEquals($this->quoteMock, $this->model->getForCustomer($customerId, $sharedStoreIds)); static::assertEquals($this->quoteMock, $this->model->getForCustomer($customerId)); - static::assertEquals($this->quoteMock, $this->model->getForCustomer($customerId)); + } + + /** + * Checking how many times we invoke setSharedStoreIds() in protected method loadQuote() + * + * @return array + */ + public function getForCustomerDataProvider() + { + return [ + [ + 'invoke_number_times' => static::never(), + 'shared_store_ids' => [] + ], + [ + 'invoke_number_times' => static::once(), + 'shared_store_ids' => [1] + ] + ]; } /** diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index 4ea067c9be8f6..a590c8aa891a5 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -314,6 +314,43 @@ public function testDispatchWithCustomerCountryInEU() $this->model->execute($this->observerMock); } + public function testDispatchWithAddressCustomerVatIdAndCountryId() + { + $customerCountryCode = "BE"; + $customerVat = "123123123"; + $defaultShipping = 1; + + $customerAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $customerAddress->expects($this->any()) + ->method("getVatId") + ->willReturn($customerVat); + + $customerAddress->expects($this->any()) + ->method("getCountryId") + ->willReturn($customerCountryCode); + + $this->addressRepository->expects($this->once()) + ->method("getById") + ->with($defaultShipping) + ->willReturn($customerAddress); + + $this->customerMock->expects($this->atLeastOnce()) + ->method("getDefaultShipping") + ->willReturn($defaultShipping); + + $this->vatValidatorMock->expects($this->once()) + ->method('isEnabled') + ->with($this->quoteAddressMock, $this->storeId) + ->will($this->returnValue(true)); + + $this->customerVatMock->expects($this->once()) + ->method('isCountryInEU') + ->with($customerCountryCode) + ->willReturn(true); + + $this->model->execute($this->observerMock); + } + public function testDispatchWithEmptyShippingAddress() { $customerCountryCode = "DE"; diff --git a/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php index c19606a7b8f5d..f06f5466df91f 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php @@ -5,75 +5,116 @@ */ namespace Magento\Quote\Test\Unit\Observer; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Payment; +use Magento\Quote\Observer\SubmitObserver; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection; +use Psr\Log\LoggerInterface; + +/** + * Class SubmitObserverTest + */ class SubmitObserverTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Quote\Observer\SubmitObserver + * @var SubmitObserver */ - protected $model; + private $model; /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $loggerMock; + private $loggerMock; /** - * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender|\PHPUnit_Framework_MockObject_MockObject + * @var OrderSender|\PHPUnit_Framework_MockObject_MockObject */ - protected $orderSenderMock; + private $orderSenderMock; /** - * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject + * @var InvoiceSender|\PHPUnit_Framework_MockObject_MockObject */ - protected $observerMock; + private $invoiceSender; /** - * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject + * @var Observer|\PHPUnit_Framework_MockObject_MockObject */ - protected $quoteMock; + private $observerMock; /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + * @var Quote|\PHPUnit_Framework_MockObject_MockObject */ - protected $orderMock; + private $quoteMock; /** - * @var \Magento\Quote\Model\Quote\Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Order|\PHPUnit_Framework_MockObject_MockObject */ - protected $paymentMock; + private $orderMock; + + /** + * @var Payment|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentMock; protected function setUp() { - $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class); - $this->paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); - $this->orderSenderMock = - $this->createMock(\Magento\Sales\Model\Order\Email\Sender\OrderSender::class); - $eventMock = $this->getMockBuilder(\Magento\Framework\Event::class) + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderSenderMock = $this->createMock(OrderSender::class); + $this->invoiceSender = $this->createMock(InvoiceSender::class); + $eventMock = $this->getMockBuilder(Event::class) ->disableOriginalConstructor() ->setMethods(['getQuote', 'getOrder']) ->getMock(); - $this->observerMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']); + $this->observerMock = $this->createPartialMock(Observer::class, ['getEvent']); $this->observerMock->expects($this->any())->method('getEvent')->willReturn($eventMock); $eventMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock); $eventMock->expects($this->once())->method('getOrder')->willReturn($this->orderMock); $this->quoteMock->expects($this->once())->method('getPayment')->willReturn($this->paymentMock); - $this->model = new \Magento\Quote\Observer\SubmitObserver( + $this->model = new SubmitObserver( $this->loggerMock, - $this->orderSenderMock + $this->orderSenderMock, + $this->invoiceSender ); } + /** + * Tests successful email sending. + */ public function testSendEmail() { - $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(''); - $this->orderMock->expects($this->once())->method('getCanSendNewEmailFlag')->willReturn(true); - $this->orderSenderMock->expects($this->once())->method('send')->willReturn(true); - $this->loggerMock->expects($this->never())->method('critical'); + $this->paymentMock->method('getOrderPlaceRedirectUrl')->willReturn(''); + $invoice = $this->createMock(Invoice::class); + $invoiceCollection = $this->createMock(Collection::class); + $invoiceCollection->method('getItems') + ->willReturn([$invoice]); + + $this->orderMock->method('getInvoiceCollection') + ->willReturn($invoiceCollection); + $this->orderMock->method('getCanSendNewEmailFlag')->willReturn(true); + $this->orderSenderMock->expects($this->once()) + ->method('send')->willReturn(true); + $this->invoiceSender->expects($this->once()) + ->method('send') + ->with($invoice) + ->willReturn(true); + $this->loggerMock->expects($this->never()) + ->method('critical'); + $this->model->execute($this->observerMock); } + /** + * Tests failing email sending. + */ public function testFailToSendEmail() { $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(''); @@ -85,6 +126,9 @@ public function testFailToSendEmail() $this->model->execute($this->observerMock); } + /** + * Tests send email when redirect. + */ public function testSendEmailWhenRedirectUrlExists() { $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(false); diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml index b4c75fc1d21d0..d41591c619cde 100644 --- a/app/code/Magento/Quote/etc/db_schema.xml +++ b/app/code/Magento/Quote/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP"/> @@ -27,7 +27,7 @@ <column xsi:type="decimal" name="items_qty" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Items Qty"/> <column xsi:type="int" name="orig_order_id" padding="10" unsigned="true" nullable="true" identity="false" - default="0" comment="Orig Order Id"/> + default="0" comment="Orig Order ID"/> <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Store To Base Rate"/> <column xsi:type="decimal" name="store_to_quote_rate" scale="4" precision="12" unsigned="false" nullable="true" @@ -43,11 +43,11 @@ default="0" comment="Base Grand Total"/> <column xsi:type="varchar" name="checkout_method" nullable="true" length="255" comment="Checkout Method"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="customer_tax_class_id" padding="10" unsigned="true" nullable="true" - identity="false" comment="Customer Tax Class Id"/> + identity="false" comment="Customer Tax Class ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="true" identity="false" - default="0" comment="Customer Group Id"/> + default="0" comment="Customer Group ID"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="255" comment="Customer Email"/> <column xsi:type="varchar" name="customer_prefix" nullable="true" length="40" comment="Customer Prefix"/> <column xsi:type="varchar" name="customer_firstname" nullable="true" length="255" comment="Customer Firstname"/> @@ -63,7 +63,7 @@ identity="false" default="0" comment="Customer Is Guest"/> <column xsi:type="varchar" name="remote_ip" nullable="true" length="45" comment="Remote Ip"/> <column xsi:type="varchar" name="applied_rule_ids" nullable="true" length="255" comment="Applied Rule Ids"/> - <column xsi:type="varchar" name="reserved_order_id" nullable="true" length="64" comment="Reserved Order Id"/> + <column xsi:type="varchar" name="reserved_order_id" nullable="true" length="64" comment="Reserved Order ID"/> <column xsi:type="varchar" name="password_hash" nullable="true" length="255" comment="Password Hash"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="255" comment="Coupon Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="255" @@ -103,19 +103,19 @@ </table> <table name="quote_address" resource="checkout" engine="innodb" comment="Sales Flat Quote Address"> <column xsi:type="int" name="address_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Address Id"/> + comment="Address ID"/> <column xsi:type="int" name="quote_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Quote Id"/> + default="0" comment="Quote ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="smallint" name="save_in_address_book" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Save In Address Book"/> <column xsi:type="int" name="customer_address_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Address Id"/> + comment="Customer Address ID"/> <column xsi:type="varchar" name="address_type" nullable="true" length="10" comment="Address Type"/> <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> <column xsi:type="varchar" name="prefix" nullable="true" length="40" comment="Prefix"/> @@ -129,9 +129,9 @@ <column xsi:type="varchar" name="city" nullable="true" length="255"/> <column xsi:type="varchar" name="region" nullable="true" length="255"/> <column xsi:type="int" name="region_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Region Id"/> + comment="Region ID"/> <column xsi:type="varchar" name="postcode" nullable="true" length="20" comment="Postcode"/> - <column xsi:type="varchar" name="country_id" nullable="true" length="30" comment="Country Id"/> + <column xsi:type="varchar" name="country_id" nullable="true" length="30" comment="Country ID"/> <column xsi:type="varchar" name="telephone" nullable="true" length="255"/> <column xsi:type="varchar" name="fax" nullable="true" length="255"/> <column xsi:type="smallint" name="same_as_billing" padding="5" unsigned="true" nullable="false" identity="false" @@ -195,10 +195,10 @@ comment="Shipping Incl Tax"/> <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> - <column xsi:type="text" name="vat_id" nullable="true" comment="Vat Id"/> + <column xsi:type="text" name="vat_id" nullable="true" comment="Vat ID"/> <column xsi:type="smallint" name="vat_is_valid" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Is Valid"/> - <column xsi:type="text" name="vat_request_id" nullable="true" comment="Vat Request Id"/> + <column xsi:type="text" name="vat_request_id" nullable="true" comment="Vat Request ID"/> <column xsi:type="text" name="vat_request_date" nullable="true" comment="Vat Request Date"/> <column xsi:type="smallint" name="vat_request_success" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Request Success"/> @@ -215,19 +215,19 @@ </table> <table name="quote_item" resource="checkout" engine="innodb" comment="Sales Flat Quote Item"> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="int" name="quote_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Quote Id"/> + default="0" comment="Quote ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="parent_item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Item Id"/> + comment="Parent Item ID"/> <column xsi:type="smallint" name="is_virtual" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Virtual"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> @@ -315,13 +315,13 @@ </table> <table name="quote_address_item" resource="checkout" engine="innodb" comment="Sales Flat Quote Address Item"> <column xsi:type="int" name="address_item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Address Item Id"/> + comment="Address Item ID"/> <column xsi:type="int" name="parent_item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Item Id"/> + comment="Parent Item ID"/> <column xsi:type="int" name="quote_address_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Quote Address Id"/> + default="0" comment="Quote Address ID"/> <column xsi:type="int" name="quote_item_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Quote Item Id"/> + default="0" comment="Quote Item ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -349,13 +349,13 @@ <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="super_product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Super Product Id"/> + comment="Super Product ID"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> <column xsi:type="varchar" name="image" nullable="true" length="255" comment="Image"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> @@ -413,11 +413,11 @@ </table> <table name="quote_item_option" resource="checkout" engine="innodb" comment="Sales Flat Quote Item Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -431,9 +431,9 @@ </table> <table name="quote_payment" resource="checkout" engine="innodb" comment="Sales Flat Quote Payment"> <column xsi:type="int" name="payment_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Payment Id"/> + comment="Payment ID"/> <column xsi:type="int" name="quote_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Quote Id"/> + default="0" comment="Quote ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -467,9 +467,9 @@ </table> <table name="quote_shipping_rate" resource="checkout" engine="innodb" comment="Sales Flat Quote Shipping Rate"> <column xsi:type="int" name="rate_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rate Id"/> + comment="Rate ID"/> <column xsi:type="int" name="address_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Address Id"/> + default="0" comment="Address ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml index cd5e62307fdca..f66001e7789cf 100644 --- a/app/code/Magento/Quote/etc/di.xml +++ b/app/code/Magento/Quote/etc/di.xml @@ -18,7 +18,7 @@ <preference for="Magento\Quote\Api\Data\CartInterface" type="Magento\Quote\Model\Quote" /> <preference for="Magento\Quote\Api\CartItemRepositoryInterface" type="Magento\Quote\Model\Quote\Item\Repository" /> <preference for="Magento\Quote\Api\CartRepositoryInterface" type="Magento\Quote\Model\QuoteRepository" /> - <preference for="Magento\Quote\Api\Data\CartSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Quote\Api\Data\CartSearchResultsInterface" type="Magento\Quote\Model\CartSearchResults" /> <preference for="Magento\Quote\Api\PaymentMethodManagementInterface" type="Magento\Quote\Model\PaymentMethodManagement" /> <preference for="Magento\Quote\Api\Data\PaymentInterface" type="Magento\Quote\Model\Quote\Payment" /> <preference for="Magento\Quote\Api\CouponManagementInterface" type="Magento\Quote\Model\CouponManagement" /> diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php index 005cf3a10ca80..91c77a1a3ecc5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -8,7 +8,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\Message\AbstractMessage; +use Magento\Framework\Message\MessageInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\Quote; @@ -55,29 +55,15 @@ public function execute(Quote $cart, array $cartItems): void } if ($cart->getData('has_error')) { - throw new GraphQlInputException( - __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) - ); + $e = new GraphQlInputException(__('Shopping cart errors')); + $errors = $cart->getErrors(); + foreach ($errors as $error) { + /** @var MessageInterface $error */ + $e->addError(new GraphQlInputException(__($error->getText()))); + } + throw $e; } $this->cartRepository->save($cart); } - - /** - * Collecting cart errors - * - * @param Quote $cart - * @return string - */ - private function getCartErrors(Quote $cart): string - { - $errorMessages = []; - - /** @var AbstractMessage $error */ - foreach ($cart->getErrors() as $error) { - $errorMessages[] = $error->getText(); - } - - return implode(PHP_EOL, $errorMessages); - } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 11719db2d1b8f..83c1d03f132db 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -72,7 +72,12 @@ public function execute(Quote $cart, array $cartItemData): void } if (is_string($result)) { - throw new GraphQlInputException(__($result)); + $e = new GraphQlInputException(__('Cannot add product to cart')); + $errors = array_unique(explode("\n", $result)); + foreach ($errors as $error) { + $e->addError(new GraphQlInputException(__($error))); + } + throw $e; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/SaveQuoteAddressToCustomerAddressBook.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/SaveQuoteAddressToCustomerAddressBook.php new file mode 100644 index 0000000000000..c87101156327e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/SaveQuoteAddressToCustomerAddressBook.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart\Address; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\RegionInterface; +use Magento\Customer\Api\Data\RegionInterfaceFactory; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Quote\Model\Quote\Address as QuoteAddress; + +/** + * Save Address to Customer Address Book. + */ +class SaveQuoteAddressToCustomerAddressBook +{ + /** + * @var AddressInterfaceFactory + */ + private $addressFactory; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var RegionInterfaceFactory + */ + private $regionFactory; + + /** + * @param AddressInterfaceFactory $addressFactory + * @param AddressRepositoryInterface $addressRepository + * @param RegionInterfaceFactory $regionFactory + */ + public function __construct( + AddressInterfaceFactory $addressFactory, + AddressRepositoryInterface $addressRepository, + RegionInterfaceFactory $regionFactory + ) { + $this->addressFactory = $addressFactory; + $this->addressRepository = $addressRepository; + $this->regionFactory = $regionFactory; + } + + /** + * Save Address to Customer Address Book. + * + * @param QuoteAddress $quoteAddress + * @param int $customerId + * + * @return void + * @throws GraphQlInputException + */ + public function execute(QuoteAddress $quoteAddress, int $customerId): void + { + try { + /** @var AddressInterface $customerAddress */ + $customerAddress = $this->addressFactory->create(); + $customerAddress->setFirstname($quoteAddress->getFirstname()) + ->setLastname($quoteAddress->getLastname()) + ->setMiddlename($quoteAddress->getMiddlename()) + ->setPrefix($quoteAddress->getPrefix()) + ->setSuffix($quoteAddress->getSuffix()) + ->setVatId($quoteAddress->getVatId()) + ->setCountryId($quoteAddress->getCountryId()) + ->setCompany($quoteAddress->getCompany()) + ->setRegionId($quoteAddress->getRegionId()) + ->setFax($quoteAddress->getFax()) + ->setCity($quoteAddress->getCity()) + ->setPostcode($quoteAddress->getPostcode()) + ->setStreet($quoteAddress->getStreet()) + ->setTelephone($quoteAddress->getTelephone()) + ->setCustomerId($customerId); + + /** @var RegionInterface $region */ + $region = $this->regionFactory->create(); + $region->setRegionCode($quoteAddress->getRegionCode()) + ->setRegion($quoteAddress->getRegion()) + ->setRegionId($quoteAddress->getRegionId()); + $customerAddress->setRegion($region); + + $this->addressRepository->save($customerAddress); + } catch (InputException $inputException) { + $graphQlInputException = new GraphQlInputException(__($inputException->getMessage())); + $errors = $inputException->getErrors(); + foreach ($errors as $error) { + $graphQlInputException->addError(new GraphQlInputException(__($error->getMessage()))); + } + throw $graphQlInputException; + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage()), $exception); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php index dd6478b4873c6..22bd0abb1a159 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php @@ -7,7 +7,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; @@ -39,20 +39,20 @@ public function __construct( * * @param CartInterface $cart * @param AddressInterface $billingAddress - * @param bool $useForShipping + * @param bool $sameAsShipping * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException */ public function execute( CartInterface $cart, AddressInterface $billingAddress, - bool $useForShipping + bool $sameAsShipping ): void { try { - $this->billingAddressManagement->assign($cart->getId(), $billingAddress, $useForShipping); + $this->billingAddressManagement->assign($cart->getId(), $billingAddress, $sameAsShipping); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); - } catch (LocalizedException $e) { + } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php index 527999b245a4c..4dbcfad31e84c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php @@ -7,7 +7,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; @@ -50,7 +50,7 @@ public function execute( $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); - } catch (LocalizedException $e) { + } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php index cd61b52db1beb..77925d0940cf4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php @@ -13,10 +13,10 @@ interface BuyRequestDataProviderInterface { /** - * Build buy request for adding product to cart + * Provide buy request data from add to cart item request * * @param array $cartItemData - * @return DataObject + * @return array */ public function execute(array $cartItemData): array; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php index c4d795293220f..468ef4b8f879c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php @@ -7,7 +7,6 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Customer\Model\Address\AbstractAddress; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; @@ -41,33 +40,46 @@ public function execute(QuoteAddress $address): array $addressData = $this->dataObjectConverter->toFlatArray($address, [], AddressInterface::class); $addressData['model'] = $address; - $addressData = array_merge($addressData, [ - 'country' => [ - 'code' => $address->getCountryId(), - 'label' => $address->getCountry() - ], - 'region' => [ - 'code' => $address->getRegionCode(), - 'label' => $address->getRegion() - ], - 'street' => $address->getStreet(), - 'items_weight' => $address->getWeight(), - 'customer_notes' => $address->getCustomerNotes() - ]); + $addressData = array_merge( + $addressData, + [ + 'country' => [ + 'code' => $address->getCountryId(), + 'label' => $address->getCountry() + ], + 'region' => [ + 'code' => $address->getRegionCode(), + 'label' => $address->getRegion() + ], + 'street' => $address->getStreet(), + 'items_weight' => $address->getWeight(), + 'customer_notes' => $address->getCustomerNotes() + ] + ); if (!$address->hasItems()) { return $addressData; } - $addressItemsData = []; foreach ($address->getAllItems() as $addressItem) { - $addressItemsData[] = [ - 'cart_item_id' => $addressItem->getQuoteItemId(), + if ($addressItem instanceof \Magento\Quote\Model\Quote\Item) { + $itemId = $addressItem->getItemId(); + } else { + $itemId = $addressItem->getQuoteItemId(); + } + $productData = $addressItem->getProduct()->getData(); + $productData['model'] = $addressItem->getProduct(); + $addressData['cart_items'][] = [ + 'cart_item_id' => $itemId, 'quantity' => $addressItem->getQty() ]; + $addressData['cart_items_v2'][] = [ + 'id' => $itemId, + 'quantity' => $addressItem->getQty(), + 'product' => $productData, + 'model' => $addressItem, + ]; } - $addressData['cart_items'] = $addressItemsData; - return $addressData; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetShippingAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetShippingAddress.php new file mode 100644 index 0000000000000..2a57c281de183 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetShippingAddress.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Model\Quote\Address; +use Magento\QuoteGraphQl\Model\Cart\Address\SaveQuoteAddressToCustomerAddressBook; + +/** + * Get shipping address + */ +class GetShippingAddress +{ + /** + * @var QuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var SaveQuoteAddressToCustomerAddressBook + */ + private $saveQuoteAddressToCustomerAddressBook; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param SaveQuoteAddressToCustomerAddressBook $saveQuoteAddressToCustomerAddressBook + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + SaveQuoteAddressToCustomerAddressBook $saveQuoteAddressToCustomerAddressBook + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->saveQuoteAddressToCustomerAddressBook = $saveQuoteAddressToCustomerAddressBook; + } + + /** + * Get Shipping Address based on the input. + * + * @param ContextInterface $context + * @param array $shippingAddressInput + * @return Address + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, array $shippingAddressInput): Address + { + $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; + $addressInput = $shippingAddressInput['address'] ?? null; + + if ($addressInput) { + $addressInput['customer_notes'] = $shippingAddressInput['customer_notes'] ?? ''; + } + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The shipping address must contain either "customer_address_id" or "address".') + ); + } + + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + + $shippingAddress = $this->createShippingAddress($context, $customerAddressId, $addressInput); + + return $shippingAddress; + } + + /** + * Create shipping address. + * + * @param ContextInterface $context + * @param int|null $customerAddressId + * @param array|null $addressInput + * + * @return \Magento\Quote\Model\Quote\Address + * @throws GraphQlAuthorizationException + */ + private function createShippingAddress( + ContextInterface $context, + ?int $customerAddressId, + ?array $addressInput + ) { + $customerId = $context->getUserId(); + + if (null === $customerAddressId) { + $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + + // need to save address only for registered user and if save_in_address_book = true + if (0 !== $customerId + && isset($addressInput['save_in_address_book']) + && (bool)$addressInput['save_in_address_book'] === true + ) { + $this->saveQuoteAddressToCustomerAddressBook->execute($shippingAddress, $customerId); + } + } else { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( + (int)$customerAddressId, + $customerId + ); + } + + return $shippingAddress; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php index afc88f026ed62..9cb3d9173ac59 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php @@ -15,6 +15,9 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Quote\Model\Quote\Address as QuoteAddress; use Magento\Quote\Model\Quote\AddressFactory as BaseQuoteAddressFactory; +use Magento\Directory\Model\ResourceModel\Region\CollectionFactory as RegionCollectionFactory; +use Magento\Directory\Helper\Data as CountryHelper; +use Magento\Directory\Model\AllowedCountries; /** * Create QuoteAddress @@ -36,36 +39,77 @@ class QuoteAddressFactory */ private $addressHelper; + /** + * @var RegionCollectionFactory + */ + private $regionCollectionFactory; + + /** + * @var CountryHelper + */ + private $countryHelper; + + /** + * @var AllowedCountries + */ + private $allowedCountries; + /** * @param BaseQuoteAddressFactory $quoteAddressFactory * @param GetCustomerAddress $getCustomerAddress * @param AddressHelper $addressHelper + * @param RegionCollectionFactory $regionCollectionFactory + * @param CountryHelper $countryHelper + * @param AllowedCountries $allowedCountries */ public function __construct( BaseQuoteAddressFactory $quoteAddressFactory, GetCustomerAddress $getCustomerAddress, - AddressHelper $addressHelper + AddressHelper $addressHelper, + RegionCollectionFactory $regionCollectionFactory, + CountryHelper $countryHelper, + AllowedCountries $allowedCountries ) { $this->quoteAddressFactory = $quoteAddressFactory; $this->getCustomerAddress = $getCustomerAddress; $this->addressHelper = $addressHelper; + $this->regionCollectionFactory = $regionCollectionFactory; + $this->countryHelper = $countryHelper; + $this->allowedCountries = $allowedCountries; } /** * Create QuoteAddress based on input data * * @param array $addressInput + * * @return QuoteAddress * @throws GraphQlInputException */ public function createBasedOnInputData(array $addressInput): QuoteAddress { $addressInput['country_id'] = ''; - if ($addressInput['country_code']) { + if (isset($addressInput['country_code']) && $addressInput['country_code']) { $addressInput['country_code'] = strtoupper($addressInput['country_code']); $addressInput['country_id'] = $addressInput['country_code']; } + $allowedCountries = $this->allowedCountries->getAllowedCountries(); + if (!in_array($addressInput['country_code'], $allowedCountries, true)) { + throw new GraphQlInputException(__('Country is not available')); + } + $isRegionRequired = $this->countryHelper->isRegionRequired($addressInput['country_code']); + if ($isRegionRequired && !empty($addressInput['region'])) { + $regionCollection = $this->regionCollectionFactory + ->create() + ->addRegionCodeFilter($addressInput['region']) + ->addCountryFilter($addressInput['country_code']); + if ($regionCollection->getSize() === 0) { + throw new GraphQlInputException( + __('Region is not available for the selected country') + ); + } + } $maxAllowedLineCount = $this->addressHelper->getStreetLines(); if (is_array($addressInput['street']) && count($addressInput['street']) > $maxAllowedLineCount) { throw new GraphQlInputException( diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php index 673debefd0874..20a3677ef1feb 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -13,6 +13,7 @@ use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Quote\Address; +use Magento\QuoteGraphQl\Model\Cart\Address\SaveQuoteAddressToCustomerAddressBook; /** * Set billing address for a specified shopping cart @@ -29,16 +30,24 @@ class SetBillingAddressOnCart */ private $assignBillingAddressToCart; + /** + * @var SaveQuoteAddressToCustomerAddressBook + */ + private $saveQuoteAddressToCustomerAddressBook; + /** * @param QuoteAddressFactory $quoteAddressFactory * @param AssignBillingAddressToCart $assignBillingAddressToCart + * @param SaveQuoteAddressToCustomerAddressBook $saveQuoteAddressToCustomerAddressBook */ public function __construct( QuoteAddressFactory $quoteAddressFactory, - AssignBillingAddressToCart $assignBillingAddressToCart + AssignBillingAddressToCart $assignBillingAddressToCart, + SaveQuoteAddressToCustomerAddressBook $saveQuoteAddressToCustomerAddressBook ) { $this->quoteAddressFactory = $quoteAddressFactory; $this->assignBillingAddressToCart = $assignBillingAddressToCart; + $this->saveQuoteAddressToCustomerAddressBook = $saveQuoteAddressToCustomerAddressBook; } /** @@ -56,8 +65,11 @@ public function execute(ContextInterface $context, CartInterface $cart, array $b { $customerAddressId = $billingAddressInput['customer_address_id'] ?? null; $addressInput = $billingAddressInput['address'] ?? null; - $useForShipping = isset($billingAddressInput['use_for_shipping']) + // Need to keep this for BC of `use_for_shipping` field + $sameAsShipping = isset($billingAddressInput['use_for_shipping']) ? (bool)$billingAddressInput['use_for_shipping'] : false; + $sameAsShipping = isset($billingAddressInput['same_as_shipping']) + ? (bool)$billingAddressInput['same_as_shipping'] : $sameAsShipping; if (null === $customerAddressId && null === $addressInput) { throw new GraphQlInputException( @@ -72,15 +84,15 @@ public function execute(ContextInterface $context, CartInterface $cart, array $b } $addresses = $cart->getAllShippingAddresses(); - if ($useForShipping && count($addresses) > 1) { + if ($sameAsShipping && count($addresses) > 1) { throw new GraphQlInputException( - __('Using the "use_for_shipping" option with multishipping is not possible.') + __('Using the "same_as_shipping" option with multishipping is not possible.') ); } $billingAddress = $this->createBillingAddress($context, $customerAddressId, $addressInput); - $this->assignBillingAddressToCart->execute($cart, $billingAddress, $useForShipping); + $this->assignBillingAddressToCart->execute($cart, $billingAddress, $sameAsShipping); } /** @@ -101,6 +113,15 @@ private function createBillingAddress( ): Address { if (null === $customerAddressId) { $billingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + + $customerId = $context->getUserId(); + // need to save address only for registered user and if save_in_address_book = true + if (0 !== $customerId + && isset($addressInput['save_in_address_book']) + && (bool)$addressInput['save_in_address_book'] === true + ) { + $this->saveQuoteAddressToCustomerAddressBook->execute($billingAddress, $customerId); + } } else { if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); @@ -111,6 +132,16 @@ private function createBillingAddress( (int)$context->getUserId() ); } + $errors = $billingAddress->validate(); + + if (true !== $errors) { + $e = new GraphQlInputException(__('Billing address errors')); + foreach ($errors as $error) { + $e->addError(new GraphQlInputException($error)); + } + throw $e; + } + return $billingAddress; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 77719bed5b16f..e058913dde1d3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -7,10 +7,10 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote\Address; /** * Set single shipping address for a specified shopping cart @@ -18,25 +18,25 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface { /** - * @var QuoteAddressFactory + * @var AssignShippingAddressToCart */ - private $quoteAddressFactory; + private $assignShippingAddressToCart; /** - * @var AssignShippingAddressToCart + * @var GetShippingAddress */ - private $assignShippingAddressToCart; + private $getShippingAddress; /** - * @param QuoteAddressFactory $quoteAddressFactory * @param AssignShippingAddressToCart $assignShippingAddressToCart + * @param GetShippingAddress $getShippingAddress */ public function __construct( - QuoteAddressFactory $quoteAddressFactory, - AssignShippingAddressToCart $assignShippingAddressToCart + AssignShippingAddressToCart $assignShippingAddressToCart, + GetShippingAddress $getShippingAddress ) { - $this->quoteAddressFactory = $quoteAddressFactory; $this->assignShippingAddressToCart = $assignShippingAddressToCart; + $this->getShippingAddress = $getShippingAddress; } /** @@ -50,34 +50,18 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s ); } $shippingAddressInput = current($shippingAddressesInput); - $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; - $addressInput = $shippingAddressInput['address'] ?? null; - if (null === $customerAddressId && null === $addressInput) { - throw new GraphQlInputException( - __('The shipping address must contain either "customer_address_id" or "address".') - ); - } + $shippingAddress = $this->getShippingAddress->execute($context, $shippingAddressInput); - if ($customerAddressId && $addressInput) { - throw new GraphQlInputException( - __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') - ); - } + $errors = $shippingAddress->validate(); - if (null === $customerAddressId) { - $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); - } else { - if (false === $context->getExtensionAttributes()->getIsCustomer()) { - throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + if (true !== $errors) { + $e = new GraphQlInputException(__('Shipping address errors')); + foreach ($errors as $error) { + $e->addError(new GraphQlInputException($error)); } - - $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( - (int)$customerAddressId, - $context->getUserId() - ); + throw $e; } - $this->assignShippingAddressToCart->execute($cart, $shippingAddress); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php b/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php index 81a7779eb98b5..7b5c9a57a7be9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/UpdateCartItem.php @@ -93,8 +93,6 @@ public function execute(Quote $cart, int $cartItemId, float $quantity, array $cu ) ); } - - $this->quoteRepository->save($cart); } /** @@ -105,7 +103,7 @@ public function execute(Quote $cart, int $cartItemId, float $quantity, array $cu * @param float $quantity * @throws GraphQlNoSuchEntityException * @throws NoSuchEntityException - * @throws GraphQlNoSuchEntityException + * @throws GraphQlInputException */ private function updateItemQuantity(int $itemId, Quote $cart, float $quantity) { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateAddressFromSchema.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateAddressFromSchema.php new file mode 100644 index 0000000000000..69fb7d3c2fe28 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ValidateAddressFromSchema.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; + +/** + * Validates address against required fields from schema + */ +class ValidateAddressFromSchema +{ + /** + * @var TypeRegistry + */ + private $typeRegistry; + + /** + * @param TypeRegistry $typeRegistry + */ + public function __construct( + TypeRegistry $typeRegistry + ) { + $this->typeRegistry = $typeRegistry; + } + + /** + * Validate data from address against mandatory fields from graphql schema for address + * + * @param array $address + * @return bool + */ + public function execute(array $address = []) : bool + { + /** @var \Magento\Framework\GraphQL\Schema\Type\Input\InputObjectType $cartAddressInput */ + $cartAddressInput = $this->typeRegistry->get('CartAddressInput'); + $fields = $cartAddressInput->getFields(); + + foreach ($fields as $field) { + if ($field->getType() instanceof \Magento\Framework\GraphQL\Schema\Type\NonNull) { + // an array key has to exist but it's value should not be null + if (array_key_exists($field->name, $address) + && !is_array($address[$field->name]) + && !isset($address[$field->name])) { + return false; + } + } + } + return true; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php new file mode 100644 index 0000000000000..fa232f4d9cd6c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Api\CouponManagementInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * @inheritdoc + */ +class AppliedCoupons implements ResolverInterface +{ + /** + * @var CouponManagementInterface + */ + private $couponManagement; + + /** + * @param CouponManagementInterface $couponManagement + */ + public function __construct( + CouponManagementInterface $couponManagement + ) { + $this->couponManagement = $couponManagement; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $cart = $value['model']; + $cartId = $cart->getId(); + $appliedCoupons = []; + $appliedCoupon = $this->couponManagement->get($cartId); + if ($appliedCoupon) { + $appliedCoupons[] = [ 'code' => $appliedCoupon ]; + } + return !empty($appliedCoupons) ? $appliedCoupons : null; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php index a6bb0b0d04df1..b0e621789ebd0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php @@ -13,6 +13,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Api\Data\CartInterface; use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; +use Magento\QuoteGraphQl\Model\Cart\ValidateAddressFromSchema; /** * @inheritdoc @@ -24,12 +25,21 @@ class BillingAddress implements ResolverInterface */ private $extractQuoteAddressData; + /** + * @var ValidateAddressFromSchema + */ + private $validateAddressFromSchema; + /** * @param ExtractQuoteAddressData $extractQuoteAddressData + * @param ValidateAddressFromSchema $validateAddressFromSchema */ - public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) - { + public function __construct( + ExtractQuoteAddressData $extractQuoteAddressData, + ValidateAddressFromSchema $validateAddressFromSchema + ) { $this->extractQuoteAddressData = $extractQuoteAddressData; + $this->validateAddressFromSchema = $validateAddressFromSchema; } /** @@ -44,11 +54,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $value['model']; $billingAddress = $cart->getBillingAddress(); - if (null === $billingAddress) { + $addressData = $this->extractQuoteAddressData->execute($billingAddress); + if (!$this->validateAddressFromSchema->execute($addressData)) { return null; } - - $addressData = $this->extractQuoteAddressData->execute($billingAddress); return $addressData; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartIsVirtual.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartIsVirtual.php new file mode 100644 index 0000000000000..3aec0a8365da8 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartIsVirtual.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class CartIsVirtual implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Quote $cart */ + $cart = $value['model']; + + return (bool)$cart->getIsVirtual(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index a591c74e78db3..f0d97780845e8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -55,7 +55,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value // But the totals should be calculated even if no address is set $this->totals = $this->totalsCollector->collectQuoteTotals($cartItem->getQuote()); } - $currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode(); return [ @@ -71,6 +70,40 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value 'currency' => $currencyCode, 'value' => $cartItem->getRowTotalInclTax(), ], + 'total_item_discount' => [ + 'currency' => $currencyCode, + 'value' => $cartItem->getDiscountAmount(), + ], + 'discounts' => $this->getDiscountValues($cartItem, $currencyCode) ]; } + + /** + * Get Discount Values + * + * @param Item $cartItem + * @param string $currencyCode + * @return array + */ + private function getDiscountValues($cartItem, $currencyCode) + { + $itemDiscounts = $cartItem->getExtensionAttributes()->getDiscounts(); + if ($itemDiscounts) { + $discountValues=[]; + foreach ($itemDiscounts as $value) { + $discount = []; + $amount = []; + /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discountData */ + $discountData = $value->getDiscountData(); + $discountAmount = $discountData->getAmount(); + $discount['label'] = $value->getRuleLabel() ?: __('Discount'); + $amount['value'] = $discountAmount; + $amount['currency'] = $currencyCode; + $discount['amount'] = $amount; + $discountValues[] = $discount; + } + return $discountValues; + } + return null; + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php new file mode 100644 index 0000000000000..0be95eccc39e5 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; + +/** + * Get cart for the customer + */ +class CustomerCart implements ResolverInterface +{ + /** + * @var CreateEmptyCartForCustomer + */ + private $createEmptyCartForCustomer; + + /** + * @var CartManagementInterface + */ + private $cartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedQuoteId; + + /** + * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer + * @param CartManagementInterface $cartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + */ + public function __construct( + CreateEmptyCartForCustomer $createEmptyCartForCustomer, + CartManagementInterface $cartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + ) { + $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; + $this->cartManagement = $cartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $currentUserId = $context->getUserId(); + + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The request is allowed for logged in customer')); + } + try { + $cart = $this->cartManagement->getCartForCustomer($currentUserId); + } catch (NoSuchEntityException $e) { + $this->createEmptyCartForCustomer->execute($currentUserId, null); + $cart = $this->cartManagement->getCartForCustomer($currentUserId); + } + + $maskedId = $this->quoteIdToMaskedQuoteId->execute((int) $cart->getId()); + if (empty($maskedId)) { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId((int) $cart->getId()); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + } + + return [ + 'model' => $cart + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php new file mode 100644 index 0000000000000..703015fd7ddb1 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class Discounts implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $quote = $value['model']; + + return $this->getDiscountValues($quote); + } + + /** + * Get Discount Values + * + * @param Quote $quote + * @return array + */ + private function getDiscountValues(Quote $quote) + { + $discountValues=[]; + $address = $quote->getShippingAddress(); + $totalDiscounts = $address->getExtensionAttributes()->getDiscounts(); + if ($totalDiscounts && is_array($totalDiscounts)) { + foreach ($totalDiscounts as $value) { + $discount = []; + $amount = []; + $discount['label'] = $value->getRuleLabel() ?: __('Discount'); + /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discountData */ + $discountData = $value->getDiscountData(); + $amount['value'] = $discountData->getAmount(); + $amount['currency'] = $quote->getQuoteCurrencyCode(); + $discount['amount'] = $amount; + $discountValues[] = $discount; + } + return $discountValues; + } + return null; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php new file mode 100644 index 0000000000000..755f79569f09a --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; + +/** + * Get cart id from the cart + */ +class MaskedCartId implements ResolverInterface +{ + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedQuoteId; + + /** + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + */ + public function __construct( + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + ) { + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Quote $cart */ + $cart = $value['model']; + $cartId = (int) $cart->getId(); + $maskedCartId = $this->getQuoteMaskId($cartId); + return $maskedCartId; + } + + /** + * Get masked id for cart + * + * @param int $quoteId + * @return string + * @throws GraphQlNoSuchEntityException + */ + private function getQuoteMaskId(int $quoteId): string + { + try { + $maskedId = $this->quoteIdToMaskedQuoteId->execute($quoteId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__('Current user does not have an active cart.')); + } + return $maskedId; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php new file mode 100644 index 0000000000000..d77d19df55603 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; + +/** + * Merge Carts Resolver + */ +class MergeCarts implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @param GetCartForUser $getCartForUser + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + GetCartForUser $getCartForUser, + CartRepositoryInterface $cartRepository + ) { + $this->getCartForUser = $getCartForUser; + $this->cartRepository = $cartRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (empty($args['source_cart_id'])) { + throw new GraphQlInputException(__('Required parameter "source_cart_id" is missing')); + } + + if (empty($args['destination_cart_id'])) { + throw new GraphQlInputException(__('Required parameter "destination_cart_id" is missing')); + } + + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + $guestMaskedCartId = $args['source_cart_id']; + $customerMaskedCartId = $args['destination_cart_id']; + + $currentUserId = $context->getUserId(); + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + // passing customerId as null enforces source cart should always be a guestcart + $guestCart = $this->getCartForUser->execute($guestMaskedCartId, null, $storeId); + $customerCart = $this->getCartForUser->execute($customerMaskedCartId, $currentUserId, $storeId); + $customerCart->merge($guestCart); + $guestCart->setIsActive(false); + $this->cartRepository->save($customerCart); + $this->cartRepository->save($guestCart); + return [ + 'model' => $customerCart, + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php index 1a0740a75c8f8..3a10c773c5f22 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php @@ -89,6 +89,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'order' => [ + 'order_number' => $order->getIncrementId(), + // @deprecated The order_id field is deprecated, use order_number instead 'order_id' => $order->getIncrementId(), ], ]; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index 0efbde5d6b218..dd4ce8fe7f7a6 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -20,7 +20,11 @@ use Magento\Sales\Api\OrderRepositoryInterface; /** - * @inheritdoc + * Resolver for setting payment method and placing order + * + * @deprecated Should use setPaymentMethodOnCart and placeOrder mutations in single request. + * @see \Magento\QuoteGraphQl\Model\Resolver\SetPaymentMethodOnCart + * @see \Magento\QuoteGraphQl\Model\Resolver\PlaceOrder */ class SetPaymentAndPlaceOrder implements ResolverInterface { @@ -95,6 +99,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'order' => [ + 'order_number' => $order->getIncrementId(), + // @deprecated The order_id field is deprecated, use order_number instead 'order_id' => $order->getIncrementId(), ], ]; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php index fb51d17b042df..429fda816efd3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -31,44 +31,37 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Address $address */ $address = $value['model']; $rates = $address->getAllShippingRates(); - $carrierTitle = null; - $methodTitle = null; + $carrierTitle = ''; + $methodTitle = ''; - if (count($rates) > 0 && !empty($address->getShippingMethod())) { - list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); + if (!count($rates) || empty($address->getShippingMethod())) { + return null; + } - /** @var Rate $rate */ - foreach ($rates as $rate) { - if ($rate->getCode() == $address->getShippingMethod()) { - $carrierTitle = $rate->getCarrierTitle(); - $methodTitle = $rate->getMethodTitle(); - break; - } - } + list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); - $data = [ - 'carrier_code' => $carrierCode, - 'method_code' => $methodCode, - 'carrier_title' => $carrierTitle, - 'method_title' => $methodTitle, - 'amount' => [ - 'value' => $address->getShippingAmount(), - 'currency' => $address->getQuote()->getQuoteCurrencyCode(), - ], - /** @deprecated The field should not be used on the storefront */ - 'base_amount' => null, - ]; - } else { - $data = [ - 'carrier_code' => null, - 'method_code' => null, - 'carrier_title' => $carrierTitle, - 'method_title' => $methodTitle, - 'amount' => null, - /** @deprecated The field should not be used on the storefront */ - 'base_amount' => null, - ]; + /** @var Rate $rate */ + foreach ($rates as $rate) { + if ($rate->getCode() == $address->getShippingMethod()) { + $carrierTitle = $rate->getCarrierTitle(); + $methodTitle = $rate->getMethodTitle(); + break; + } } + + $data = [ + 'carrier_code' => $carrierCode, + 'method_code' => $methodCode, + 'carrier_title' => $carrierTitle, + 'method_title' => $methodTitle, + 'amount' => [ + 'value' => $address->getShippingAmount(), + 'currency' => $address->getQuote()->getQuoteCurrencyCode(), + ], + /** @deprecated The field should not be used on the storefront */ + 'base_amount' => null, + ]; + return $data; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php index eb3b0966740eb..ac0bfe36627ce 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php @@ -13,6 +13,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; +use Magento\QuoteGraphQl\Model\Cart\ValidateAddressFromSchema; /** * @inheritdoc @@ -24,12 +25,21 @@ class ShippingAddresses implements ResolverInterface */ private $extractQuoteAddressData; + /** + * @var ValidateAddressFromSchema + */ + private $validateAddressFromSchema; + /** * @param ExtractQuoteAddressData $extractQuoteAddressData + * @param ValidateAddressFromSchema $validateAddressFromSchema */ - public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) - { + public function __construct( + ExtractQuoteAddressData $extractQuoteAddressData, + ValidateAddressFromSchema $validateAddressFromSchema + ) { $this->extractQuoteAddressData = $extractQuoteAddressData; + $this->validateAddressFromSchema = $validateAddressFromSchema; } /** @@ -48,7 +58,11 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (count($shippingAddresses)) { foreach ($shippingAddresses as $shippingAddress) { - $addressesData[] = $this->extractQuoteAddressData->execute($shippingAddress); + $address = $this->extractQuoteAddressData->execute($shippingAddress); + + if ($this->validateAddressFromSchema->execute($address)) { + $addressesData[] = $address; + } } } return $addressesData; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index 8066c28e9e48a..fa90f08e4b553 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -15,6 +15,7 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; @@ -39,19 +40,27 @@ class UpdateCartItems implements ResolverInterface */ private $cartItemRepository; + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + /** * @param GetCartForUser $getCartForUser * @param CartItemRepositoryInterface $cartItemRepository * @param UpdateCartItem $updateCartItem + * @param CartRepositoryInterface $cartRepository */ public function __construct( GetCartForUser $getCartForUser, CartItemRepositoryInterface $cartItemRepository, - UpdateCartItem $updateCartItem + UpdateCartItem $updateCartItem, + CartRepositoryInterface $cartRepository ) { $this->getCartForUser = $getCartForUser; $this->cartItemRepository = $cartItemRepository; $this->updateCartItem = $updateCartItem; + $this->cartRepository = $cartRepository; } /** @@ -76,6 +85,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->processCartItems($cart, $cartItems); + $this->cartRepository->save($cart); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); } catch (LocalizedException $e) { @@ -106,6 +116,11 @@ private function processCartItems(Quote $cart, array $items): void $itemId = (int)$item['cart_item_id']; $customizableOptions = $item['customizable_options'] ?? []; + $cartItem = $cart->getItemById($itemId); + if ($cartItem && $cartItem->getParentItemId()) { + throw new GraphQlInputException(__('Child items may not be updated.')); + } + if (count($customizableOptions) === 0 && !isset($item['quantity'])) { throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); } diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 08bb78ba776c4..d334d56c85aac 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -3,6 +3,7 @@ type Query { cart(cart_id: String!): Cart @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart") @doc(description:"Returns information about shopping cart") @cache(cacheable: false) + customerCart: Cart! @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CustomerCart") @doc(description:"Returns information about the customer shopping cart") @cache(cacheable: false) } type Mutation { @@ -18,7 +19,8 @@ type Mutation { setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") - setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") + setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") + mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\MergeCarts") placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") } @@ -85,6 +87,7 @@ input SetShippingAddressesOnCartInput { input ShippingAddressInput { customer_address_id: Int # If provided then will be used address from address book address: CartAddressInput + customer_notes: String } input SetBillingAddressOnCartInput { @@ -95,7 +98,8 @@ input SetBillingAddressOnCartInput { input BillingAddressInput { customer_address_id: Int address: CartAddressInput - use_for_shipping: Boolean + use_for_shipping: Boolean @doc(description: "Deprecated: use `same_as_shipping` field instead") + same_as_shipping: Boolean @doc(description: "Set billing address same as shipping") } input CartAddressInput { @@ -108,7 +112,7 @@ input CartAddressInput { postcode: String country_code: String! telephone: String! - save_in_address_book: Boolean! + save_in_address_book: Boolean } input SetShippingMethodsOnCartInput { @@ -149,9 +153,10 @@ type CartPrices { grand_total: Money subtotal_including_tax: Money subtotal_excluding_tax: Money - discount: CartDiscount + discount: CartDiscount @deprecated(reason: "Use discounts instead ") subtotal_with_discount_excluding_tax: Money applied_taxes: [CartTaxItem] + discounts: [Discount] @doc(description:"An array of applied discounts") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Discounts") } type CartTaxItem { @@ -189,61 +194,66 @@ type PlaceOrderOutput { } type Cart { + id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The ID of the cart.") items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") - applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") + applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") - billing_address: BillingCartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") + billing_address: BillingCartAddress @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") total_quantity: Float! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartTotalQuantity") + is_virtual: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartIsVirtual") } interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddressTypeResolver") { - firstname: String - lastname: String + firstname: String! + lastname: String! company: String - street: [String] - city: String + street: [String!]! + city: String! region: CartAddressRegion postcode: String - country: CartAddressCountry - telephone: String - customer_notes: String + country: CartAddressCountry! + telephone: String! } type ShippingCartAddress implements CartAddressInterface { available_shipping_methods: [AvailableShippingMethod] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\AvailableShippingMethods") selected_shipping_method: SelectedShippingMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\SelectedShippingMethod") - items_weight: Float - cart_items: [CartItemQuantity] + customer_notes: String + items_weight: Float @deprecated(reason: "This information shoud not be exposed on frontend") + cart_items: [CartItemQuantity] @deprecated(reason: "`cart_items_v2` should be used instead") + cart_items_v2: [CartItemInterface] } type BillingCartAddress implements CartAddressInterface { + customer_notes: String @deprecated (reason: "The field is used only in shipping address") } -type CartItemQuantity { - cart_item_id: Int! - quantity: Float! +type CartItemQuantity @doc(description:"Deprecated: `cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") { + cart_item_id: Int! @deprecated(reason: "`cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") + quantity: Float! @deprecated(reason: "`cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") } type CartAddressRegion { - code: String - label: String + code: String! + label: String! } type CartAddressCountry { - code: String - label: String + code: String! + label: String! } type SelectedShippingMethod { - carrier_code: String - method_code: String - carrier_title: String - method_title: String - amount: Money + carrier_code: String! + method_code: String! + carrier_title: String! + method_title: String! + amount: Money! base_amount: Money @deprecated(reason: "The field should not be used on the storefront") } @@ -318,10 +328,17 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ product: ProductInterface! } +type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") { + amount: Money! @doc(description:"The amount of the discount") + label: String! @doc(description:"A description of the discount") +} + type CartItemPrices { price: Money! row_total: Money! row_total_including_tax: Money! + discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") + total_item_discount: Money @doc(description:"The total of all discounts applied to the item") } type SelectedCustomizableOption { @@ -346,5 +363,6 @@ type CartItemSelectedOptionValuePrice { } type Order { - order_id: String + order_number: String! + order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") } diff --git a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php index 173c0a94312ee..e5084d4c9f9b6 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php @@ -7,9 +7,12 @@ namespace Magento\RelatedProductGraphQl\Model\DataProvider; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Link; use Magento\Catalog\Model\Product\LinkFactory; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\Framework\EntityManager\MetadataPool; /** * Related Products Data Provider @@ -21,13 +24,31 @@ class RelatedProductDataProvider */ private $linkFactory; + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var HydratorPool + */ + private $hydratorPool; + /** * @param LinkFactory $linkFactory + * @param MetadataPool|null $metadataPool + * @param HydratorPool|null $hydratorPool */ public function __construct( - LinkFactory $linkFactory + LinkFactory $linkFactory, + ?MetadataPool $metadataPool = null, + ?HydratorPool $hydratorPool = null ) { $this->linkFactory = $linkFactory; + $this->metadataPool = $metadataPool + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(MetadataPool::class); + $this->hydratorPool = $hydratorPool + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(HydratorPool::class); } /** @@ -62,9 +83,7 @@ public function getData(Product $product, array $fields, int $linkType): array private function getRelatedProducts(Product $product, array $fields, int $linkType): array { /** @var Link $link */ - $link = $this->linkFactory->create([ 'data' => [ - 'link_type_id' => $linkType, - ]]); + $link = $this->linkFactory->create(['data' => ['link_type_id' => $linkType]]); $collection = $link->getProductCollection(); $collection->setIsStrongMode(); @@ -75,4 +94,42 @@ private function getRelatedProducts(Product $product, array $fields, int $linkTy return $collection->getItems(); } + + /** + * Get related product IDs for given products. + * + * @param \Magento\Catalog\Api\Data\ProductInterface[] $products + * @param int $linkType + * @return string[][] keys - IDs, values - list of linked product IDs. + */ + public function getRelations(array $products, int $linkType): array + { + //Links use real IDs for root products, we need to get them + $actualIdField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $hydrator = $this->hydratorPool->getHydrator(ProductInterface::class); + /** @var ProductInterface[] $productsByActualIds */ + $productsByActualIds = []; + foreach ($products as $product) { + $productsByActualIds[$hydrator->extract($product)[$actualIdField]] = $product; + } + //Load all links + /** @var Link $link */ + $link = $this->linkFactory->create(['data' => ['link_type_id' => $linkType]]); + $collection = $link->getLinkCollection(); + $collection->addFieldToFilter('product_id', ['in' => array_keys($productsByActualIds)]); + $collection->addLinkTypeIdFilter(); + + //Prepare map + $map = []; + /** @var Link $item */ + foreach ($collection as $item) { + $productId = $productsByActualIds[$item->getProductId()]->getId(); + if (!array_key_exists($productId, $map)) { + $map[$productId] = []; + } + $map[$productId][] = $item->getLinkedProductId(); + } + + return $map; + } } diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php new file mode 100644 index 0000000000000..7ad2e5dde2985 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RelatedProductGraphQl\Model\Resolver\Batch; + +use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\BatchResolverInterface; +use Magento\Framework\GraphQl\Query\Resolver\BatchResponse; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\RelatedProductGraphQl\Model\DataProvider\RelatedProductDataProvider; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Resolve linked product lists. + */ +abstract class AbstractLikedProducts implements BatchResolverInterface +{ + /** + * @var ProductFieldsSelector + */ + private $productFieldsSelector; + + /** + * @var RelatedProductDataProvider + */ + private $relatedProductDataProvider; + + /** + * @var ProductDataProvider + */ + private $productDataProvider; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param ProductFieldsSelector $productFieldsSelector + * @param RelatedProductDataProvider $relatedProductDataProvider + * @param ProductDataProvider $productDataProvider + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + ProductFieldsSelector $productFieldsSelector, + RelatedProductDataProvider $relatedProductDataProvider, + ProductDataProvider $productDataProvider, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->productFieldsSelector = $productFieldsSelector; + $this->relatedProductDataProvider = $relatedProductDataProvider; + $this->productDataProvider = $productDataProvider; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Node type. + * + * @return string + */ + abstract protected function getNode(): string; + + /** + * Type of linked products to be resolved. + * + * @return int + */ + abstract protected function getLinkType(): int; + + /** + * Find related products. + * + * @param \Magento\Catalog\Api\Data\ProductInterface[] $products + * @param string[] $loadAttributes + * @param int $linkType + * @return \Magento\Catalog\Api\Data\ProductInterface[][] + */ + private function findRelations(array $products, array $loadAttributes, int $linkType): array + { + //Loading relations + $relations = $this->relatedProductDataProvider->getRelations($products, $linkType); + if (!$relations) { + return []; + } + $relatedIds = array_values($relations); + $relatedIds = array_unique(array_merge(...$relatedIds)); + //Loading products data. + $this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in'); + $relatedSearchResult = $this->productDataProvider->getList( + $this->searchCriteriaBuilder->create(), + $loadAttributes, + false, + true + ); + //Filling related products map. + /** @var \Magento\Catalog\Api\Data\ProductInterface[] $relatedProducts */ + $relatedProducts = []; + /** @var \Magento\Catalog\Api\Data\ProductInterface $item */ + foreach ($relatedSearchResult->getItems() as $item) { + $relatedProducts[$item->getId()] = $item; + } + + //Matching products with related products. + $relationsData = []; + foreach ($relations as $productId => $relatedIds) { + $relationsData[$productId] = array_map( + function ($id) use ($relatedProducts) { + return $relatedProducts[$id]; + }, + $relatedIds + ); + } + + return $relationsData; + } + + /** + * @inheritDoc + */ + public function resolve(ContextInterface $context, Field $field, array $requests): BatchResponse + { + /** @var \Magento\Catalog\Api\Data\ProductInterface[] $products */ + $products = []; + $fields = []; + /** @var \Magento\Framework\GraphQl\Query\Resolver\BatchRequestItemInterface $request */ + foreach ($requests as $request) { + //Gathering fields and relations to load. + if (empty($request->getValue()['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $products[] = $request->getValue()['model']; + $fields[] = $this->productFieldsSelector->getProductFieldsFromInfo($request->getInfo(), $this->getNode()); + } + $fields = array_unique(array_merge(...$fields)); + + //Finding relations. + $related = $this->findRelations($products, $fields, $this->getLinkType()); + + //Matching requests with responses. + $response = new BatchResponse(); + /** @var \Magento\Framework\GraphQl\Query\Resolver\BatchRequestItemInterface $request */ + foreach ($requests as $request) { + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $request->getValue()['model']; + $result = []; + if (array_key_exists($product->getId(), $related)) { + $result = array_map( + function ($relatedProduct) { + $data = $relatedProduct->getData(); + $data['model'] = $relatedProduct; + + return $data; + }, + $related[$product->getId()] + ); + } + $response->addResponse($request, $result); + } + + return $response; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/CrossSellProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/CrossSellProducts.php new file mode 100644 index 0000000000000..d636d980597c6 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/CrossSellProducts.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RelatedProductGraphQl\Model\Resolver\Batch; + +use Magento\Catalog\Model\Product\Link; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\BatchResponse; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * CrossSell Products Resolver + */ +class CrossSellProducts extends AbstractLikedProducts +{ + /** + * @inheritDoc + */ + protected function getNode(): string + { + return 'crosssell_products'; + } + + /** + * @inheritDoc + */ + protected function getLinkType(): int + { + return Link::LINK_TYPE_CROSSSELL; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/RelatedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/RelatedProducts.php new file mode 100644 index 0000000000000..cefa4db912328 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/RelatedProducts.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RelatedProductGraphQl\Model\Resolver\Batch; + +use Magento\Catalog\Model\Product\Link; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\BatchResponse; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Related Products Resolver + */ +class RelatedProducts extends AbstractLikedProducts +{ + /** + * @inheritDoc + */ + protected function getNode(): string + { + return 'related_products'; + } + + /** + * @inheritDoc + */ + protected function getLinkType(): int + { + return Link::LINK_TYPE_RELATED; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/UpSellProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/UpSellProducts.php new file mode 100644 index 0000000000000..42807772a2282 --- /dev/null +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/UpSellProducts.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RelatedProductGraphQl\Model\Resolver\Batch; + +use Magento\Catalog\Model\Product\Link; + +/** + * UpSell Products Resolver + */ +class UpSellProducts extends AbstractLikedProducts +{ + /** + * @inheritDoc + */ + protected function getNode(): string + { + return 'upsell_products'; + } + + /** + * @inheritDoc + */ + protected function getLinkType(): int + { + return Link::LINK_TYPE_UPSELL; + } +} diff --git a/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls b/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls index 81c51f5035ea6..849f8fb679806 100644 --- a/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/RelatedProductGraphQl/etc/schema.graphqls @@ -2,7 +2,7 @@ # See COPYING.txt for license details. interface ProductInterface { - related_products: [ProductInterface] @doc(description: "Related Products") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\RelatedProducts") - upsell_products: [ProductInterface] @doc(description: "Upsell Products") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\UpSellProducts") - crosssell_products: [ProductInterface] @doc(description: "Crosssell Products") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\CrossSellProducts") + related_products: [ProductInterface] @doc(description: "Related Products") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\Batch\\RelatedProducts") + upsell_products: [ProductInterface] @doc(description: "Upsell Products") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\Batch\\UpSellProducts") + crosssell_products: [ProductInterface] @doc(description: "Crosssell Products") @resolver(class: "Magento\\RelatedProductGraphQl\\Model\\Resolver\\Batch\\CrossSellProducts") } diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php index 2fbff13a5b644..d5d8d32744e49 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php @@ -18,6 +18,7 @@ /** * Reports api controller * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 * @SuppressWarnings(PHPMD.AllPurposeAction) @@ -140,7 +141,7 @@ protected function _showLastExecutionTime($flagCode, $refreshCode) $flag = $this->_objectManager->create(\Magento\Reports\Model\Flag::class) ->setReportFlagCode($flagCode) ->loadSelf(); - $updatedAt = 'undefined'; + $updatedAt = __('Never'); if ($flag->hasData()) { $updatedAt = $this->timezone->formatDate( $flag->getLastUpdate(), diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php index 8c985238efe47..ab76cad8da5c4 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php @@ -77,7 +77,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -111,7 +111,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php index ec514f45ff65a..5b4cf39d65def 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php @@ -44,7 +44,7 @@ abstract class AbstractCollection extends \Magento\Catalog\Model\ResourceModel\P * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -69,7 +69,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php index c02de01117a74..39d673911111f 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php @@ -63,7 +63,7 @@ class Collection extends \Magento\Reports\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -94,7 +94,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/Reports/Setup/Patch/Data/ReportDisableNotification.php b/app/code/Magento/Reports/Setup/Patch/Data/ReportDisableNotification.php new file mode 100644 index 0000000000000..e34d7f1144341 --- /dev/null +++ b/app/code/Magento/Reports/Setup/Patch/Data/ReportDisableNotification.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Setup\Patch\Data; + +use Magento\Framework\Notification\NotifierInterface; + +/** + * Report Disable Notification + */ +class ReportDisableNotification implements \Magento\Framework\Setup\Patch\DataPatchInterface +{ + /** + * @var NotifierInterface + */ + private $notifier; + + /** + * @param NotifierInterface $notifier + */ + public function __construct( + NotifierInterface $notifier + ) { + $this->notifier = $notifier; + } + + /** + * @inheritdoc + */ + public function apply() + { + $message = <<<"MESSAGE" +To improve performance, collecting statistics for the Magento Report module is disabled by default. +You can enable it in System Config. +MESSAGE; + $this->notifier->addNotice(__('Disable Notice'), __($message)); + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } +} diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml index 29bbe91afbd11..1250c7b0ac370 100644 --- a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml @@ -16,29 +16,11 @@ <argument name="orderFromDate" type="string"/> <argument name="orderToDate" type="string"/> </arguments> - + <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere"/> <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Any" stepKey="selectAnyOption"/> <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport"/> </actionGroup> - - <actionGroup name="GenerateOrderReportForNotCancelActionGroup"> - <annotations> - <description>Clicks on 'here' to refresh the grid data. Enters the provided Order From/To Dates and provided Order Status. Clicks on 'Show Report'.</description> - </annotations> - <arguments> - <argument name="orderFromDate" type="string"/> - <argument name="orderToDate" type="string"/> - <argument name="statuses" type="string"/> - </arguments> - - <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere"/> - <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> - <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> - <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Specified" stepKey="selectSpecifiedOption"/> - <selectOption selector="{{OrderReportFilterSection.orderStatusSpecified}}" parameterArray="{{statuses}}" stepKey="selectSpecifiedOptionStatus"/> - <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportForNotCancelActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportForNotCancelActionGroup.xml new file mode 100644 index 0000000000000..32c82308c8f99 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportForNotCancelActionGroup.xml @@ -0,0 +1,28 @@ +<?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="GenerateOrderReportForNotCancelActionGroup"> + <annotations> + <description>Clicks on 'here' to refresh the grid data. Enters the provided Order From/To Dates and provided Order Status. Clicks on 'Show Report'.</description> + </annotations> + <arguments> + <argument name="orderFromDate" type="string"/> + <argument name="orderToDate" type="string"/> + <argument name="statuses" type="string"/> + </arguments> + + <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere"/> + <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> + <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Specified" stepKey="selectSpecifiedOption"/> + <selectOption selector="{{OrderReportFilterSection.orderStatusSpecified}}" parameterArray="{{statuses}}" stepKey="selectSpecifiedOptionStatus"/> + <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/Page/ReportsSearchTermsPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/ReportsSearchTermsPage.xml new file mode 100644 index 0000000000000..e9e60c6d6dccb --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/ReportsSearchTermsPage.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="ReportsSearchTermPage" url="search/term/report/" area="admin" module="Magento_Reports"> + <section name="ReportsSearchTermSection"/> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 3f1857b352dbc..23067457081a6 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -29,7 +29,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; -use Magento\Framework\Module\ModuleManagerInterface as Manager; +use Magento\Framework\Module\Manager as Manager; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; diff --git a/app/code/Magento/Reports/etc/config.xml b/app/code/Magento/Reports/etc/config.xml index ffd2299eb6884..5e4bf56cff50c 100644 --- a/app/code/Magento/Reports/etc/config.xml +++ b/app/code/Magento/Reports/etc/config.xml @@ -20,7 +20,7 @@ <mtd_start>1</mtd_start> </dashboard> <options> - <enabled>1</enabled> + <enabled>0</enabled> <product_view_enabled>1</product_view_enabled> <product_send_enabled>1</product_send_enabled> <product_compare_enabled>1</product_compare_enabled> diff --git a/app/code/Magento/Reports/etc/db_schema.xml b/app/code/Magento/Reports/etc/db_schema.xml index 1321ebba4d3d6..30accf36a053e 100644 --- a/app/code/Magento/Reports/etc/db_schema.xml +++ b/app/code/Magento/Reports/etc/db_schema.xml @@ -10,15 +10,15 @@ <table name="report_compared_product_index" resource="default" engine="innodb" comment="Reports Compared Product Index Table"> <column xsi:type="bigint" name="index_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Index Id"/> + comment="Index ID"/> <column xsi:type="int" name="visitor_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Visitor Id"/> + comment="Visitor ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="timestamp" name="added_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Added At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -54,15 +54,15 @@ <table name="report_viewed_product_index" resource="default" engine="innodb" comment="Reports Viewed Product Index Table"> <column xsi:type="bigint" name="index_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Index Id"/> + comment="Index ID"/> <column xsi:type="int" name="visitor_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Visitor Id"/> + comment="Visitor ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="timestamp" name="added_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Added At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -97,7 +97,7 @@ </table> <table name="report_event_types" resource="default" engine="innodb" comment="Reports Event Type Table"> <column xsi:type="smallint" name="event_type_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Event Type Id"/> + comment="Event Type ID"/> <column xsi:type="varchar" name="event_name" nullable="false" length="64" comment="Event Name"/> <column xsi:type="smallint" name="customer_login" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Customer Login"/> @@ -107,19 +107,19 @@ </table> <table name="report_event" resource="default" engine="innodb" comment="Reports Event Table"> <column xsi:type="bigint" name="event_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Event Id"/> + comment="Event ID"/> <column xsi:type="timestamp" name="logged_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Logged At"/> <column xsi:type="smallint" name="event_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Event Type Id"/> + default="0" comment="Event Type ID"/> <column xsi:type="int" name="object_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Object Id"/> + default="0" comment="Object ID"/> <column xsi:type="int" name="subject_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Subject Id"/> + default="0" comment="Subject ID"/> <column xsi:type="smallint" name="subtype" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Subtype"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="event_id"/> </constraint> @@ -146,12 +146,12 @@ </table> <table name="report_viewed_product_aggregated_daily" resource="default" engine="innodb" comment="Most Viewed Products Aggregated Daily"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -182,12 +182,12 @@ </table> <table name="report_viewed_product_aggregated_monthly" resource="default" engine="innodb" comment="Most Viewed Products Aggregated Monthly"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -218,12 +218,12 @@ </table> <table name="report_viewed_product_aggregated_yearly" resource="default" engine="innodb" comment="Most Viewed Products Aggregated Yearly"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> diff --git a/app/code/Magento/Reports/i18n/en_US.csv b/app/code/Magento/Reports/i18n/en_US.csv index 3225f2fc41409..169d3cc2b74b4 100644 --- a/app/code/Magento/Reports/i18n/en_US.csv +++ b/app/code/Magento/Reports/i18n/en_US.csv @@ -224,3 +224,5 @@ Action,Action Report,Report Description,Description undefined,undefined +Never,Never + diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml index 55ca286ad3d47..2d33ca3c6a872 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml @@ -40,6 +40,7 @@ <argument name="id" xsi:type="string">accounts</argument> <argument name="column_css_class" xsi:type="string">col-qty</argument> <argument name="header_css_class" xsi:type="string">col-qty</argument> + <argument name="sortable" xsi:type="string">0</argument> </arguments> </block> </referenceBlock> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml index 649dc7ceeb065..5b841e3523649 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml @@ -71,7 +71,7 @@ <argument name="sortable" xsi:type="string">0</argument> <argument name="id" xsi:type="string">updated_at</argument> <argument name="index" xsi:type="string">updated_at</argument> - <argument name="default" xsi:type="string" translate="true">undefined</argument> + <argument name="default" xsi:type="string" translate="true">Never</argument> <argument name="column_css_class" xsi:type="string">col-period</argument> <argument name="header_css_class" xsi:type="string">col-period</argument> </arguments> diff --git a/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php b/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php index 509e826e6f7d3..d3bbdf9a7eb40 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php +++ b/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php @@ -29,7 +29,7 @@ class Grid extends \Magento\Catalog\Block\Adminhtml\Product\Grid * @param \Magento\Catalog\Model\Product\Type $type * @param \Magento\Catalog\Model\Product\Attribute\Source\Status $status * @param \Magento\Catalog\Model\Product\Visibility $visibility - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Store\Model\ResourceModel\Website\CollectionFactory $websitesFactory * @param array $data * @@ -44,7 +44,7 @@ public function __construct( \Magento\Catalog\Model\Product\Type $type, \Magento\Catalog\Model\Product\Attribute\Source\Status $status, \Magento\Catalog\Model\Product\Visibility $visibility, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Store\Model\ResourceModel\Website\CollectionFactory $websitesFactory, array $data = [] ) { diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php index e4b4da23ac629..a8a39b3326edd 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php @@ -111,13 +111,16 @@ protected function addRatingFieldset() ] ); - foreach ($this->systemStore->getStoreCollection() as $store) { - $this->getFieldset('rating_form')->addField( - 'rating_code_' . $store->getId(), - 'text', - ['label' => $store->getName(), 'name' => 'rating_codes[' . $store->getId() . ']'] - ); + if (!$this->_storeManager->isSingleStoreMode()) { + foreach ($this->systemStore->getStoreCollection() as $store) { + $this->getFieldset('rating_form')->addField( + 'rating_code_' . $store->getId(), + 'text', + ['label' => $store->getName(), 'name' => 'rating_codes[' . $store->getId() . ']'] + ); + } } + $this->setRatingData(); } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php index 1b9c9eaa22be7..14a3271e7196d 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php @@ -33,7 +33,7 @@ public function execute() try { $this->getModel()->aggregate()->delete(); - $this->messageManager->addSuccess(__('The review has been deleted.')); + $this->messageManager->addSuccessMessage(__('The review has been deleted.')); if ($this->getRequest()->getParam('ret') == 'pending') { $resultRedirect->setPath('review/*/pending'); } else { @@ -41,9 +41,9 @@ public function execute() } return $resultRedirect; } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong deleting this review.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong deleting this review.')); } return $resultRedirect->setPath('review/*/edit/', ['id' => $reviewId]); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php index 95f9ca3aa79d2..44b267dc5aa7c 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php @@ -59,19 +59,19 @@ public function execute() { $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { - $this->messageManager->addError(__('Please select review(s).')); + $this->messageManager->addErrorMessage(__('Please select review(s).')); } else { try { foreach ($this->getCollection() as $model) { $model->delete(); } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been deleted.', count($reviewsIds)) ); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while deleting these records.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong while deleting these records.')); } } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php index 9e93fb8fce63e..ff4acfb964898 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php @@ -59,20 +59,20 @@ public function execute() { $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { - $this->messageManager->addError(__('Please select review(s).')); + $this->messageManager->addErrorMessage(__('Please select review(s).')); } else { try { $status = $this->getRequest()->getParam('status'); foreach ($this->getCollection() as $model) { $model->setStatusId($status)->save()->aggregate(); } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been updated.', count($reviewsIds)) ); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while updating these review(s).') ); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php index eca37d3fe24da..1f82d4846ede3 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php @@ -5,20 +5,27 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; -class MassVisibleIn extends ProductController +/** + * Class MassVisibleIn + */ +class MassVisibleIn extends ProductController implements HttpPostActionInterface { + /** + * Execute action + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() { $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { - $this->messageManager->addError(__('Please select review(s).')); + $this->messageManager->addErrorMessage(__('Please select review(s).')); } else { try { $stores = $this->getRequest()->getParam('stores'); @@ -27,13 +34,13 @@ public function execute() $model->setSelectStores($stores); $model->save(); } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been updated.', count($reviewsIds)) ); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while updating these review(s).') ); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php index b42dd3b3063f6..96e6cc48fb496 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php @@ -56,7 +56,7 @@ public function execute() $review->aggregate(); - $this->messageManager->addSuccess(__('You saved the review.')); + $this->messageManager->addSuccessMessage(__('You saved the review.')); if ($this->getRequest()->getParam('ret') == 'pending') { $resultRedirect->setPath('review/*/pending'); } else { @@ -64,9 +64,9 @@ public function execute() } return $resultRedirect; } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving this review.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving this review.')); } } $resultRedirect->setPath('review/*/'); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 5b8ad106987e5..a7a0c96b7e48f 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -34,7 +34,7 @@ public function execute() if (($data = $this->getRequest()->getPostValue()) && ($reviewId = $this->getRequest()->getParam('id'))) { $review = $this->getModel(); if (!$review->getId()) { - $this->messageManager->addError(__('The review was removed by another user or does not exist.')); + $this->messageManager->addErrorMessage(__('The review was removed by another user or does not exist.')); } else { try { $review->addData($data)->save(); @@ -63,11 +63,11 @@ public function execute() $review->aggregate(); - $this->messageManager->addSuccess(__('You saved the review.')); + $this->messageManager->addSuccessMessage(__('You saved the review.')); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving this review.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving this review.')); } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php index b25db6e498fe0..03a73d431221f 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php @@ -9,9 +9,14 @@ use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; +/** + * Class Delete + */ class Delete extends RatingController implements HttpPostActionInterface { /** + * Delete action + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -23,9 +28,9 @@ public function execute() /** @var \Magento\Review\Model\Rating $model */ $model = $this->_objectManager->create(\Magento\Review\Model\Rating::class); $model->load($this->getRequest()->getParam('id'))->delete(); - $this->messageManager->addSuccess(__('You deleted the rating.')); + $this->messageManager->addSuccessMessage(__('You deleted the rating.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('review/rating/edit', ['id' => $this->getRequest()->getParam('id')]); return $resultRedirect; } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php index 5dd464f7eb611..ebb4b2a01e286 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php @@ -9,6 +9,9 @@ use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; +/** + * Class Save + */ class Save extends RatingController implements HttpPostActionInterface { /** @@ -58,10 +61,10 @@ public function execute() } } - $this->messageManager->addSuccess(__('You saved the rating.')); + $this->messageManager->addSuccessMessage(__('You saved the rating.')); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setRatingData(false); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_objectManager->get(\Magento\Backend\Model\Session::class) ->setRatingData($this->getRequest()->getPostValue()); $resultRedirect->setPath('review/rating/edit', ['id' => $this->getRequest()->getParam('id')]); diff --git a/app/code/Magento/Review/Controller/Product/Post.php b/app/code/Magento/Review/Controller/Product/Post.php index 32838eb6acbbb..2928fdce16c9b 100644 --- a/app/code/Magento/Review/Controller/Product/Post.php +++ b/app/code/Magento/Review/Controller/Product/Post.php @@ -10,6 +10,9 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Review\Model\Review; +/** + * Class Post + */ class Post extends ProductController implements HttpPostActionInterface { /** @@ -63,19 +66,19 @@ public function execute() } $review->aggregate(); - $this->messageManager->addSuccess(__('You submitted your review for moderation.')); + $this->messageManager->addSuccessMessage(__('You submitted your review for moderation.')); } catch (\Exception $e) { $this->reviewSession->setFormData($data); - $this->messageManager->addError(__('We can\'t post your review right now.')); + $this->messageManager->addErrorMessage(__('We can\'t post your review right now.')); } } else { $this->reviewSession->setFormData($data); if (is_array($validate)) { foreach ($validate as $errorMessage) { - $this->messageManager->addError($errorMessage); + $this->messageManager->addErrorMessage($errorMessage); } } else { - $this->messageManager->addError(__('We can\'t post your review right now.')); + $this->messageManager->addErrorMessage(__('We can\'t post your review right now.')); } } } diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating.php b/app/code/Magento/Review/Model/ResourceModel/Rating.php index 42c14e16a50e2..37a93d40b1107 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating.php @@ -29,7 +29,7 @@ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $_storeManager; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -46,7 +46,7 @@ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param Review\Summary $reviewSummary * @param string $connectionName @@ -55,7 +55,7 @@ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Psr\Log\LoggerInterface $logger, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary, $connectionName = null, diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index 7175baa92a2f8..ab264ef1b6179 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -75,7 +75,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory @@ -103,7 +103,7 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\Validator\UniversalFactory $universalFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, diff --git a/app/code/Magento/Review/Model/Rss.php b/app/code/Magento/Review/Model/Rss.php index df8a5dbb96841..f5abdbb4d3c9e 100644 --- a/app/code/Magento/Review/Model/Rss.php +++ b/app/code/Magento/Review/Model/Rss.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Review\Model; +use Magento\Framework\App\ObjectManager; + /** - * Class Rss - * @package Magento\Catalog\Model\Rss\Product + * Model Rss + * + * Class \Magento\Catalog\Model\Rss\Product\Rss */ class Rss extends \Magento\Framework\Model\AbstractModel { @@ -24,18 +30,35 @@ class Rss extends \Magento\Framework\Model\AbstractModel protected $eventManager; /** + * Rss constructor. + * * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param ReviewFactory $reviewFactory + * @param \Magento\Framework\Model\Context|null $context + * @param \Magento\Framework\Registry|null $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data */ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Review\Model\ReviewFactory $reviewFactory + \Magento\Review\Model\ReviewFactory $reviewFactory, + \Magento\Framework\Model\Context $context = null, + \Magento\Framework\Registry $registry = null, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [] ) { $this->reviewFactory = $reviewFactory; $this->eventManager = $eventManager; + $context = $context ?? ObjectManager::getInstance()->get(\Magento\Framework\Model\Context::class); + $registry = $registry ?? ObjectManager::getInstance()->get(\Magento\Framework\Registry::class); + parent::__construct($context, $registry, $resource, $resourceCollection, $data); } /** + * Get Product Collection + * * @return $this|\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getProductCollection() diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsNoActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsNoActionGroup.xml new file mode 100644 index 0000000000000..05fad32dabe51 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsNoActionGroup.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="AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsNoActionGroup"> + <annotations> + <description>If Single Store Mode is disabled, default store view title label should be displayed.</description> + </annotations> + <seeElement selector="{{AdminEditAndNewRatingSection.defaultStoreViewTitleLabel}}" stepKey="seeLabel"/> + <seeElement selector="{{AdminEditAndNewRatingSection.defaultStoreViewTitleInput}}" stepKey="seeInput"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsYesActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsYesActionGroup.xml new file mode 100644 index 0000000000000..6e5586bfa1252 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsYesActionGroup.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="AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsYesActionGroup"> + <annotations> + <description>If Single Store Mode is enabled, default store view title label should not be displayed.</description> + </annotations> + <dontSeeElement selector="{{AdminEditAndNewRatingSection.defaultStoreViewTitleLabel}}" stepKey="dontSeeLabel"/> + <dontSeeElement selector="{{AdminEditAndNewRatingSection.defaultStoreViewTitleInput}}" stepKey="dontSeeInput"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminNavigateToNewRatingFormActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminNavigateToNewRatingFormActionGroup.xml new file mode 100644 index 0000000000000..3659405c52b69 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminNavigateToNewRatingFormActionGroup.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="AdminNavigateToNewRatingFormActionGroup"> + <annotations> + <description>Open New Rating Form</description> + </annotations> + <amOnPage url="{{AdminNewRatingPage.url}}" stepKey="amOnUrlNewRatingPage"/> + <waitForPageLoad stepKey="waitForNewRatingPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml index 62c93764ab61d..1937905ae2849 100644 --- a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminSaveReviewActionGroup"> <click selector="{{AdminEditReviewSection.saveReview}}" stepKey="saveReview"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the review." stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the review." stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/Page/AdminNewRatingPage.xml b/app/code/Magento/Review/Test/Mftf/Page/AdminNewRatingPage.xml new file mode 100644 index 0000000000000..8dfc2182e228c --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Page/AdminNewRatingPage.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="AdminNewRatingPage" url="review/rating/new/" area="admin" module="Review"> + <section name="AdminEditAndNewRatingSection"/> + </page> +</pages> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/Review/Test/Mftf/Section/AdminEditAndNewRatingSection.xml similarity index 56% rename from app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml rename to app/code/Magento/Review/Test/Mftf/Section/AdminEditAndNewRatingSection.xml index 7ae3dd0ffee89..59dd3d2004790 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml +++ b/app/code/Magento/Review/Test/Mftf/Section/AdminEditAndNewRatingSection.xml @@ -8,7 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="OrdersGridSection"> - <element name="viewMostRecentOrder" type="button" selector="#container div.admin__data-grid-wrap td:nth-of-type(10) a"/> + <section name="AdminEditAndNewRatingSection"> + <element name="defaultStoreViewTitleLabel" type="text" selector=".field-rating_code_1 label"/> + <element name="defaultStoreViewTitleInput" type="input" selector=".field-rating_code_1 input"/> </section> </sections> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml new file mode 100644 index 0000000000000..77789dd172bdd --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml @@ -0,0 +1,31 @@ +<?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="AdminVerifyNewRatingFormSingleStoreModeNoTest"> + <annotations> + <features value="Review"/> + <stories value="Rating Form"/> + <title value="Verify New Rating Form if single store mode is No"/> + <description value="New Rating Form should have Default store view field if single store mode is No"/> + <severity value="MAJOR"/> + <testCaseId value="MC-21818"/> + <group value="review"/> + </annotations> + <before> + <magentoCLI command="config:set general/single_store_mode/enabled 0" stepKey="enabledSingleStoreMode"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateToNewRatingFormActionGroup" stepKey="navigateToNewRatingPage" /> + <actionGroup ref="AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsNoActionGroup" stepKey="verifyForm" /> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeYesTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeYesTest.xml new file mode 100644 index 0000000000000..e5368e9192c98 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeYesTest.xml @@ -0,0 +1,32 @@ +<?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="AdminVerifyNewRatingFormSingleStoreModeYesTest"> + <annotations> + <features value="Review"/> + <stories value="Rating Form"/> + <title value="Verify New Rating Form if single store mode is Yes"/> + <description value="New Rating Form should not have Default store view field if single store mode is Yes"/> + <severity value="MAJOR"/> + <testCaseId value="MC-21818"/> + <group value="review"/> + </annotations> + <before> + <magentoCLI command="config:set general/single_store_mode/enabled 1" stepKey="enabledSingleStoreMode"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set general/single_store_mode/enabled 0" stepKey="enabledSingleStoreMode"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateToNewRatingFormActionGroup" stepKey="navigateToNewRatingPage" /> + <actionGroup ref="AdminAssertStoreViewRatingTitleWhenSingleStoreModeIsYesActionGroup" stepKey="verifyForm" /> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php index 1526e80f8190a..e5fd52bf8cf97 100644 --- a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php +++ b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php @@ -299,7 +299,7 @@ public function testExecute() ->willReturnSelf(); $this->review->expects($this->once())->method('aggregate') ->willReturnSelf(); - $this->messageManager->expects($this->once())->method('addSuccess') + $this->messageManager->expects($this->once())->method('addSuccessMessage') ->with(__('You submitted your review for moderation.')) ->willReturnSelf(); $this->reviewSession->expects($this->once())->method('getRedirectUrl') diff --git a/app/code/Magento/Review/Test/Unit/Helper/DataTest.php b/app/code/Magento/Review/Test/Unit/Helper/DataTest.php new file mode 100644 index 0000000000000..7473018c0eaa2 --- /dev/null +++ b/app/code/Magento/Review/Test/Unit/Helper/DataTest.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Review\Test\Unit\Helper; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Review\Helper\Data as HelperData; +use Magento\Framework\Escaper; +use Magento\Framework\Filter\FilterManager; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Class \Magento\Review\Test\Unit\Helper\DataTest + */ +class DataTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManager; + + /** + * @var HelperData + */ + private $helper; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Escaper + */ + private $escaper; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|FilterManager + */ + private $filter; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Context + */ + private $context; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Setup environment + */ + protected function setUp() + { + $this->context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->filter = $this->getMockBuilder(FilterManager::class) + ->disableOriginalConstructor() + ->setMethods(['truncate']) + ->getMock(); + + $this->escaper = $this->getMockBuilder(Escaper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->once()) + ->method('getScopeConfig') + ->willReturn($this->scopeConfig); + + $this->objectManager = new ObjectManagerHelper($this); + $this->helper = $this->objectManager->getObject( + HelperData::class, + [ + 'context' => $this->context, + 'escaper' => $this->escaper, + 'filter' => $this->filter + ] + ); + } + + /** + * Test getDetail() function + */ + public function testGetDetail() + { + $origDetail = "This\nis\na\nstring"; + $expected = "This<br />"."\n"."is<br />"."\n"."a<br />"."\n"."string"; + + $this->filter->expects($this->any())->method('truncate') + ->with($origDetail, ['length' => 50]) + ->willReturn($origDetail); + + $this->assertEquals($expected, $this->helper->getDetail($origDetail)); + } + + /** + * Test getDetailHtml() function + */ + public function getDetailHtml() + { + $origDetail = "<span>This\nis\na\nstring</span>"; + $origDetailEscapeHtml = "This\nis\na\nstring"; + $expected = "This<br />"."\n"."is<br />"."\n"."a<br />"."\n"."string"; + + $this->escaper->expects($this->any())->method('escapeHtml') + ->with($origDetail) + ->willReturn($origDetailEscapeHtml); + + $this->filter->expects($this->any())->method('truncate') + ->with($origDetailEscapeHtml, ['length' => 50]) + ->willReturn($origDetailEscapeHtml); + + $this->assertEquals($expected, $this->helper->getDetail($origDetail)); + } + + /** + * Test getIsGuestAllowToWrite() function + */ + public function testGetIsGuestAllowToWrite() + { + $this->scopeConfig->expects($this->any())->method('isSetFlag') + ->with('catalog/review/allow_guest', ScopeInterface::SCOPE_STORE) + ->willReturn('1'); + + $this->assertEquals(true, $this->helper->getIsGuestAllowToWrite()); + } + + /** + * Test getReviewStatuses() function + */ + public function testGetReviewStatuses() + { + $expected = [ + 1 => __('Approved'), + 2 => __('Pending'), + 3 => __('Not Approved') + ]; + $this->assertEquals($expected, $this->helper->getReviewStatuses()); + } + + /** + * Test getReviewStatusesOptionArray() function + */ + public function testGetReviewStatusesOptionArray() + { + $expected = [ + ['value' => 1, 'label' => __('Approved')], + ['value' => 2, 'label' => __('Pending')], + ['value' => 3, 'label' => __('Not Approved')] + ]; + $this->assertEquals($expected, $this->helper->getReviewStatusesOptionArray()); + } +} diff --git a/app/code/Magento/Review/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ReviewTest.php b/app/code/Magento/Review/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ReviewTest.php index e1e5503ad475f..1000821dd1897 100644 --- a/app/code/Magento/Review/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ReviewTest.php +++ b/app/code/Magento/Review/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ReviewTest.php @@ -8,7 +8,7 @@ use Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier\AbstractModifierTest; use Magento\Framework\UrlInterface; use Magento\Review\Ui\DataProvider\Product\Form\Modifier\Review; -use \Magento\Framework\Module\Manager as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Ui\DataProvider\Modifier\ModifierInterface; /** diff --git a/app/code/Magento/Review/Ui/DataProvider/Product/Form/Modifier/Review.php b/app/code/Magento/Review/Ui/DataProvider/Product/Form/Modifier/Review.php index 433be1c860988..5f1401a201e3f 100644 --- a/app/code/Magento/Review/Ui/DataProvider/Product/Form/Modifier/Review.php +++ b/app/code/Magento/Review/Ui/DataProvider/Product/Form/Modifier/Review.php @@ -12,7 +12,7 @@ use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Ui\Component\Form; use Magento\Framework\UrlInterface; -use \Magento\Framework\Module\ModuleManagerInterface as ModuleManager; +use Magento\Framework\Module\Manager as ModuleManager; use Magento\Framework\App\ObjectManager; /** diff --git a/app/code/Magento/Review/etc/db_schema.xml b/app/code/Magento/Review/etc/db_schema.xml index d1090d413384b..7a451dbbbcf98 100644 --- a/app/code/Magento/Review/etc/db_schema.xml +++ b/app/code/Magento/Review/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="review_entity" resource="default" engine="innodb" comment="Review entities"> <column xsi:type="smallint" name="entity_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Review entity id"/> + comment="Review entity ID"/> <column xsi:type="varchar" name="entity_code" nullable="false" length="32" comment="Review entity code"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> @@ -17,7 +17,7 @@ </table> <table name="review_status" resource="default" engine="innodb" comment="Review statuses"> <column xsi:type="smallint" name="status_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Status id"/> + comment="Status ID"/> <column xsi:type="varchar" name="status_code" nullable="false" length="32" comment="Status code"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status_id"/> @@ -25,13 +25,13 @@ </table> <table name="review" resource="default" engine="innodb" comment="Review base information"> <column xsi:type="bigint" name="review_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Review id"/> + comment="Review ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Review create date"/> <column xsi:type="smallint" name="entity_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity id"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="entity_pk_value" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product id"/> + default="0" comment="Product ID"/> <column xsi:type="smallint" name="status_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Status code"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -53,16 +53,16 @@ </table> <table name="review_detail" resource="default" engine="innodb" comment="Review detail information"> <column xsi:type="bigint" name="detail_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Review detail id"/> + comment="Review detail ID"/> <column xsi:type="bigint" name="review_id" padding="20" unsigned="true" nullable="false" identity="false" - default="0" comment="Review id"/> + default="0" comment="Review ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Store id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="false" length="255" comment="Title"/> <column xsi:type="text" name="detail" nullable="false" comment="Detail description"/> <column xsi:type="varchar" name="nickname" nullable="false" length="128" comment="User nickname"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="detail_id"/> </constraint> @@ -85,11 +85,11 @@ </table> <table name="review_entity_summary" resource="default" engine="innodb" comment="Review aggregates"> <column xsi:type="bigint" name="primary_id" padding="20" unsigned="false" nullable="false" identity="true" - comment="Summary review entity id"/> + comment="Summary review entity ID"/> <column xsi:type="bigint" name="entity_pk_value" padding="20" unsigned="false" nullable="false" identity="false" - default="0" comment="Product id"/> + default="0" comment="Product ID"/> <column xsi:type="smallint" name="entity_type" padding="6" unsigned="false" nullable="false" identity="false" - default="0" comment="Entity type id"/> + default="0" comment="Entity type ID"/> <column xsi:type="smallint" name="reviews_count" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Qty of reviews"/> <column xsi:type="smallint" name="rating_summary" padding="6" unsigned="false" nullable="false" identity="false" @@ -158,9 +158,9 @@ </table> <table name="rating_option" resource="default" engine="innodb" comment="Rating options"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rating Option Id"/> + comment="Rating Option ID"/> <column xsi:type="smallint" name="rating_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Rating Id"/> + default="0" comment="Rating ID"/> <column xsi:type="varchar" name="code" nullable="false" length="32" comment="Rating Option Code"/> <column xsi:type="smallint" name="value" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Option Value"/> @@ -177,20 +177,20 @@ </table> <table name="rating_option_vote" resource="default" engine="innodb" comment="Rating option values"> <column xsi:type="bigint" name="vote_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Vote id"/> + comment="Vote ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Vote option id"/> + default="0" comment="Vote option ID"/> <column xsi:type="varchar" name="remote_ip" nullable="false" length="16" comment="Customer IP"/> <column xsi:type="bigint" name="remote_ip_long" padding="20" unsigned="false" nullable="false" identity="false" default="0" comment="Customer IP converted to long integer format"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - default="0" comment="Customer Id"/> + default="0" comment="Customer ID"/> <column xsi:type="bigint" name="entity_pk_value" padding="20" unsigned="true" nullable="false" identity="false" - default="0" comment="Product id"/> + default="0" comment="Product ID"/> <column xsi:type="smallint" name="rating_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Rating id"/> + default="0" comment="Rating ID"/> <column xsi:type="bigint" name="review_id" padding="20" unsigned="true" nullable="true" identity="false" - comment="Review id"/> + comment="Review ID"/> <column xsi:type="smallint" name="percent" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Percent amount"/> <column xsi:type="smallint" name="value" padding="6" unsigned="false" nullable="false" identity="false" @@ -209,11 +209,11 @@ </table> <table name="rating_option_vote_aggregated" resource="default" engine="innodb" comment="Rating vote aggregated"> <column xsi:type="int" name="primary_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Vote aggregation id"/> + comment="Vote aggregation ID"/> <column xsi:type="smallint" name="rating_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Rating id"/> + default="0" comment="Rating ID"/> <column xsi:type="bigint" name="entity_pk_value" padding="20" unsigned="true" nullable="false" identity="false" - default="0" comment="Product id"/> + default="0" comment="Product ID"/> <column xsi:type="int" name="vote_count" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Vote dty"/> <column xsi:type="int" name="vote_value_sum" padding="10" unsigned="true" nullable="false" identity="false" @@ -223,7 +223,7 @@ <column xsi:type="smallint" name="percent_approved" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Vote percent approved by admin"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="primary_id"/> </constraint> @@ -242,9 +242,9 @@ </table> <table name="rating_store" resource="default" engine="innodb" comment="Rating Store"> <column xsi:type="smallint" name="rating_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Rating id"/> + default="0" comment="Rating ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store id"/> + default="0" comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rating_id"/> <column name="store_id"/> @@ -259,9 +259,9 @@ </table> <table name="rating_title" resource="default" engine="innodb" comment="Rating Title"> <column xsi:type="smallint" name="rating_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Rating Id"/> + default="0" comment="Rating ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Rating Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rating_id"/> diff --git a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js index 9d1083d662d5a..999161d45b586 100644 --- a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js +++ b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js @@ -54,7 +54,7 @@ define([ event.preventDefault(); anchor = $(this).attr('href').replace(/^.*?(#|$)/, ''); - addReviewBlock = $('.block.review-add .block-content #' + anchor); + addReviewBlock = $('#' + anchor); if (addReviewBlock.length) { $('.product.data.items [data-role="content"]').each(function (index) { //eslint-disable-line diff --git a/app/code/Magento/Rss/Controller/Feed.php b/app/code/Magento/Rss/Controller/Feed.php index 8fbe7addb560d..4c5acf97bde8c 100644 --- a/app/code/Magento/Rss/Controller/Feed.php +++ b/app/code/Magento/Rss/Controller/Feed.php @@ -76,6 +76,8 @@ public function __construct( } /** + * Authenticate not logged in customer. + * * @return bool */ protected function auth() @@ -85,7 +87,6 @@ protected function auth() try { $customer = $this->customerAccountManagement->authenticate($login, $password); $this->customerSession->setCustomerDataAsLoggedIn($customer); - $this->customerSession->regenerateId(); } catch (\Exception $e) { $this->logger->critical($e); } diff --git a/app/code/Magento/Rule/Model/AbstractModel.php b/app/code/Magento/Rule/Model/AbstractModel.php index 72b3528532be5..58c18093c3bab 100644 --- a/app/code/Magento/Rule/Model/AbstractModel.php +++ b/app/code/Magento/Rule/Model/AbstractModel.php @@ -12,6 +12,7 @@ * Abstract Rule entity data model * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -342,7 +343,7 @@ protected function _convertFlatToRecursive(array $data) foreach ($value as $id => $data) { $path = explode('--', $id); $node = & $arr; - for ($i = 0, $l = sizeof($path); $i < $l; $i++) { + for ($i = 0, $l = count($path); $i < $l; $i++) { if (!isset($node[$key][$path[$i]])) { $node[$key][$path[$i]] = []; } @@ -483,6 +484,8 @@ public function getWebsiteIds() } /** + * Get extension factory + * * @return \Magento\Framework\Api\ExtensionAttributesFactory * @deprecated 100.1.0 */ @@ -493,6 +496,8 @@ private function getExtensionFactory() } /** + * Get custom attribute factory + * * @return \Magento\Framework\Api\AttributeValueFactory * @deprecated 100.1.0 */ diff --git a/app/code/Magento/Rule/Model/Action/Collection.php b/app/code/Magento/Rule/Model/Action/Collection.php index 3fd1d59df3315..33e385e264f72 100644 --- a/app/code/Magento/Rule/Model/Action/Collection.php +++ b/app/code/Magento/Rule/Model/Action/Collection.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Rule\Model\Action; /** + * Collections + * * @api * @since 100.0.2 */ @@ -61,6 +64,8 @@ public function asArray(array $arrAttributes = []) } /** + * Load array + * * @param array $arr * @return $this */ @@ -80,6 +85,8 @@ public function loadArray(array $arr) } /** + * Add actions + * * @param ActionInterface $action * @return $this */ @@ -91,7 +98,7 @@ public function addAction(ActionInterface $action) $actions[] = $action; if (!$action->getId()) { - $action->setId($this->getId() . '.' . sizeof($actions)); + $action->setId($this->getId() . '.' . count($actions)); } $this->setActions($actions); @@ -99,6 +106,8 @@ public function addAction(ActionInterface $action) } /** + * As html + * * @return string */ public function asHtml() @@ -111,6 +120,8 @@ public function asHtml() } /** + * Return new child element + * * @return $this */ public function getNewChildElement() @@ -129,6 +140,8 @@ public function getNewChildElement() } /** + * Return as html recursive + * * @return string */ public function asHtmlRecursive() @@ -142,6 +155,8 @@ public function asHtmlRecursive() } /** + * Add string + * * @param string $format * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -153,6 +168,8 @@ public function asString($format = '') } /** + * Return string as recursive + * * @param int $level * @return string */ @@ -166,6 +183,8 @@ public function asStringRecursive($level = 0) } /** + * Process + * * @return $this */ public function process() diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index 6729fe722de56..67fc3590ac501 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -17,6 +17,7 @@ * @method setFormName() * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -385,12 +386,12 @@ public function getValueSelectOptions() public function getValueParsed() { if (!$this->hasValueParsed()) { - $value = $this->getData('value'); + $value = $this->getValue(); if (is_array($value) && count($value) === 1) { $value = reset($value); } if (!is_array($value) && $this->isArrayOperatorType() && $value) { - $value = preg_split('#\s*[,;]\s*#', $value, null, PREG_SPLIT_NO_EMPTY); + $value = preg_split('#\s*[,;]\s*#', (string) $value, -1, PREG_SPLIT_NO_EMPTY); } $this->setValueParsed($value); } @@ -419,8 +420,11 @@ public function getValue() { if ($this->getInputType() == 'date' && !$this->getIsValueParsed()) { // date format intentionally hard-coded + $date = $this->getData('value'); + $date = (\is_numeric($date) ? '@' : '') . $date; $this->setValue( - (new \DateTime($this->getData('value')))->format('Y-m-d H:i:s') + (new \DateTime($date, new \DateTimeZone((string) $this->_localeDate->getConfigTimezone()))) + ->format('Y-m-d H:i:s') ); $this->setIsValueParsed(true); } @@ -432,6 +436,7 @@ public function getValue() * * @return array|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ public function getValueName() { @@ -469,6 +474,7 @@ public function getValueName() } return $value; } + //phpcs:enable Generic.Metrics.NestingLevel /** * Get inherited conditions selectors @@ -674,6 +680,9 @@ public function getValueElement() $elementParams['placeholder'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; $elementParams['autocomplete'] = 'off'; $elementParams['readonly'] = 'true'; + $elementParams['value_name'] = + (new \DateTime($elementParams['value'], new \DateTimeZone($this->_localeDate->getConfigTimezone()))) + ->format('Y-m-d'); } return $this->getForm()->addField( $this->getPrefix() . '__' . $this->getId() . '__value', @@ -756,7 +765,7 @@ public function asStringRecursive($level = 0) /** * Validate product attribute value for condition * - * @param object|array|int|string|float|bool $validatedValue product attribute value + * @param object|array|int|string|float|bool|null $validatedValue product attribute value * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -821,6 +830,7 @@ public function validateAttribute($validatedValue) case '{}': case '!{}': if (is_scalar($validatedValue) && is_array($value)) { + $validatedValue = (string)$validatedValue; foreach ($value as $item) { if (stripos($validatedValue, (string)$item) !== false) { $result = true; @@ -868,18 +878,19 @@ public function validateAttribute($validatedValue) /** * Case and type insensitive comparison of values * - * @param string|int|float $validatedValue - * @param string|int|float $value + * @param string|int|float|null $validatedValue + * @param string|int|float|null $value * @param bool $strict * @return bool */ protected function _compareValues($validatedValue, $value, $strict = true) { - if ($strict && is_numeric($validatedValue) && is_numeric($value)) { + if (null === $value || null === $validatedValue || + $strict && is_numeric($validatedValue) && is_numeric($value)) { return $validatedValue == $value; } - $validatePattern = preg_quote($validatedValue, '~'); + $validatePattern = preg_quote((string) $validatedValue, '~'); if ($strict) { $validatePattern = '^' . $validatePattern . '$'; } diff --git a/app/code/Magento/Rule/Model/Condition/Combine.php b/app/code/Magento/Rule/Model/Condition/Combine.php index 48873aec66295..a8a8e5fb0f843 100644 --- a/app/code/Magento/Rule/Model/Condition/Combine.php +++ b/app/code/Magento/Rule/Model/Condition/Combine.php @@ -6,6 +6,8 @@ namespace Magento\Rule\Model\Condition; /** + * Combine + * * @api * @since 100.0.2 */ @@ -22,6 +24,8 @@ class Combine extends AbstractCondition protected $_logger; /** + * Construct + * * @param Context $context * @param array $data */ @@ -54,6 +58,8 @@ public function __construct(Context $context, array $data = []) /* start aggregator methods */ /** + * Load aggregation options + * * @return $this */ public function loadAggregatorOptions() @@ -63,6 +69,8 @@ public function loadAggregatorOptions() } /** + * Return agregator selected options + * * @return array */ public function getAggregatorSelectOptions() @@ -75,6 +83,8 @@ public function getAggregatorSelectOptions() } /** + * Get Agregator name + * * @return string */ public function getAggregatorName() @@ -83,6 +93,8 @@ public function getAggregatorName() } /** + * Return agregator element + * * @return object */ public function getAggregatorElement() @@ -112,6 +124,8 @@ public function getAggregatorElement() /* end aggregator methods */ /** + * Load value options + * * @return $this */ public function loadValueOptions() @@ -121,6 +135,8 @@ public function loadValueOptions() } /** + * Adds condition + * * @param object $condition * @return $this */ @@ -134,7 +150,7 @@ public function addCondition($condition) $conditions[] = $condition; if (!$condition->getId()) { - $condition->setId($this->getId() . '--' . sizeof($conditions)); + $condition->setId($this->getId() . '--' . count($conditions)); } $this->setData($this->getPrefix(), $conditions); @@ -142,6 +158,8 @@ public function addCondition($condition) } /** + * Return value element type + * * @return string */ public function getValueElementType() @@ -181,6 +199,8 @@ public function asArray(array $arrAttributes = []) } /** + * As xml + * * @param string $containerKey * @param string $itemKey * @return string @@ -202,6 +222,8 @@ public function asXml($containerKey = 'conditions', $itemKey = 'condition') } /** + * Load array + * * @param array $arr * @param string $key * @return $this @@ -230,6 +252,8 @@ public function loadArray($arr, $key = 'conditions') } /** + * Load xml + * * @param array|string $xml * @return $this */ @@ -247,6 +271,8 @@ public function loadXml($xml) } /** + * As html + * * @return string */ public function asHtml() @@ -263,6 +289,8 @@ public function asHtml() } /** + * Get new child element + * * @return $this */ public function getNewChildElement() @@ -282,6 +310,8 @@ public function getNewChildElement() } /** + * As html recursive + * * @return string */ public function asHtmlRecursive() @@ -300,6 +330,8 @@ public function asHtmlRecursive() } /** + * As string + * * @param string $format * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -311,6 +343,8 @@ public function asString($format = '') } /** + * As string recursive + * * @param int $level * @return string */ @@ -324,6 +358,8 @@ public function asStringRecursive($level = 0) } /** + * Validate + * * @param \Magento\Framework\Model\AbstractModel $model * @return bool */ @@ -374,6 +410,8 @@ protected function _isValid($entity) } /** + * Set js From object + * * @param \Magento\Framework\Data\Form $form * @return $this */ diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 33e1bf97c3474..a0812aeaa9c5e 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -152,9 +152,11 @@ protected function _getMappedSqlCondition( ): string { $argument = $condition->getMappedSqlField(); - // If rule hasn't valid argument - create negative expression to prevent incorrect rule behavior. + // If rule hasn't valid argument - prevent incorrect rule behavior. if (empty($argument)) { return $this->_expressionFactory->create(['expression' => '1 = -1']); + } elseif (preg_match('/[^a-z0-9\-_\.\`]/i', $argument) > 0) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid field')); } $conditionOperator = $condition->getOperatorForValidate(); @@ -195,7 +197,6 @@ protected function _getMappedSqlCondition( ); } } - return $this->_expressionFactory->create( ['expression' => $expression] ); @@ -241,6 +242,7 @@ protected function _getMappedSqlCombination( * @param AbstractCollection $collection * @param Combine $combine * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ public function attachConditionToCollection( AbstractCollection $collection, @@ -250,29 +252,42 @@ public function attachConditionToCollection( $this->_joinTablesToCollection($collection, $combine); $whereExpression = (string)$this->_getMappedSqlCombination($combine); if (!empty($whereExpression)) { - if (!empty($combine->getConditions())) { - $conditions = ''; - $attributeField = ''; - foreach ($combine->getConditions() as $condition) { - if ($condition->getData('attribute') === \Magento\Catalog\Api\Data\ProductInterface::SKU) { - $conditions = $condition->getData('value'); - $attributeField = $condition->getMappedSqlField(); - } - } - - $collection->getSelect()->where($whereExpression); + $collection->getSelect()->where($whereExpression); + $this->buildConditions($collection, $combine); + } + } - if (!empty($conditions) && !empty($attributeField)) { - $conditions = explode(',', $conditions); - foreach ($conditions as &$condition) { - $condition = "'" . trim($condition) . "'"; - } - $conditions = implode(', ', $conditions); - $collection->getSelect()->order("FIELD($attributeField, $conditions)"); + /** + * Build sql conditions from combination. + * + * @param AbstractCollection $collection + * @param Combine $combine + * @return void + */ + private function buildConditions(AbstractCollection $collection, Combine $combine) : void + { + if (!empty($combine->getConditions())) { + $conditions = ''; + $attributeField = ''; + foreach ($combine->getConditions() as $condition) { + if ($condition->getData('attribute') === \Magento\Catalog\Api\Data\ProductInterface::SKU + && $condition->getData('operator') === '()' + ) { + $conditions = $condition->getData('value'); + $attributeField = $this->_connection->quoteIdentifier($condition->getMappedSqlField()); } - } else { - // Select ::where method adds braces even on empty expression - $collection->getSelect()->where($whereExpression); + } + + if (!empty($conditions) && !empty($attributeField)) { + $conditions = $this->_connection->quote( + array_map('trim', explode(',', $conditions)) + ); + $collection->getSelect()->reset(Select::ORDER); + $collection->getSelect()->order( + $this->_expressionFactory->create( + ['expression' => "FIELD($attributeField, $conditions)"] + ) + ); } } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php index 12e59e63f6f7c..1efa149b390ef 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php @@ -136,4 +136,12 @@ public function getFormValues() { return $this->_getAddress()->getData(); } + + /** + * @inheritDoc + */ + protected function getAddressStoreId() + { + return $this->_getAddress()->getOrder()->getStoreId(); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php index e4b9dd4c63b93..bcdeb4e7d67de 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php @@ -14,6 +14,7 @@ /** * Order create address form + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Address extends \Magento\Sales\Block\Adminhtml\Order\Create\Form\AbstractForm @@ -216,15 +217,12 @@ public function getAddressCollectionJson() * Prepare Form and add elements to form * * @return $this - * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _prepareForm() { - $storeId = $this->getCreateOrderModel() - ->getSession() - ->getStoreId(); + $storeId = $this->getAddressStoreId(); $this->_storeManager->setCurrentStore($storeId); $fieldset = $this->_form->addFieldset('main', ['no_container' => true]); @@ -271,21 +269,24 @@ protected function _prepareForm() $this->_form->setValues($this->getFormValues()); - if ($this->_form->getElement('country_id')->getValue()) { - $countryId = $this->_form->getElement('country_id')->getValue(); - $this->_form->getElement('country_id')->setValue(null); - foreach ($this->_form->getElement('country_id')->getValues() as $country) { + $countryElement = $this->_form->getElement('country_id'); + + $this->processCountryOptions($countryElement); + + if ($countryElement->getValue()) { + $countryId = $countryElement->getValue(); + $countryElement->setValue(null); + foreach ($countryElement->getValues() as $country) { if ($country['value'] == $countryId) { - $this->_form->getElement('country_id')->setValue($countryId); + $countryElement->setValue($countryId); } } } - if ($this->_form->getElement('country_id')->getValue() === null) { - $this->_form->getElement('country_id')->setValue( + if ($countryElement->getValue() === null) { + $countryElement->setValue( $this->directoryHelper->getDefaultCountry($this->getStore()) ); } - $this->processCountryOptions($this->_form->getElement('country_id')); // Set custom renderer for VAT field if needed $vatIdElement = $this->_form->getElement('vat_id'); if ($vatIdElement && $this->getDisplayVatValidationButton() !== false) { @@ -309,7 +310,7 @@ protected function _prepareForm() */ private function processCountryOptions(\Magento\Framework\Data\Form\Element\AbstractElement $countryElement) { - $storeId = $this->getBackendQuoteSession()->getStoreId(); + $storeId = $this->getAddressStoreId(); $options = $this->getCountriesCollection() ->loadByStore($storeId) ->toOptionArray(); @@ -388,4 +389,14 @@ public function getAddressAsString(\Magento\Customer\Api\Data\AddressInterface $ return $this->escapeHtml($result); } + + /** + * Return address store id. + * + * @return int + */ + protected function getAddressStoreId() + { + return $this->getBackendQuoteSession()->getStoreId(); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php index 4a335805f8a1e..b314ee24c3e27 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php @@ -97,7 +97,7 @@ public function getItems() } } - if (sizeof($items)) { + if (count($items)) { return $items; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 9a271f741edda..001c581dc0dac 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,8 +5,7 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; -use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection - as ProductCollectionDataProvider; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection; use Magento\Framework\App\ObjectManager; /** @@ -48,7 +47,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended protected $_productFactory; /** - * @var ProductCollectionDataProvider $productCollectionProvider + * @var ProductCollection $productCollectionProvider */ private $productCollectionProvider; @@ -60,7 +59,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data - * @param ProductCollectionDataProvider|null $productCollectionProvider + * @param ProductCollection|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -70,14 +69,14 @@ public function __construct( \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, array $data = [], - ProductCollectionDataProvider $productCollectionProvider = null + ProductCollection $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; $this->productCollectionProvider = $productCollectionProvider - ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); + ?: ObjectManager::getInstance()->get(ProductCollection::class); parent::__construct($context, $backendHelper, $data); } @@ -94,6 +93,7 @@ protected function _construct() $this->setCheckboxCheckCallback('order.productGridCheckboxCheck.bind(order)'); $this->setRowInitCallback('order.productGridRowInit.bind(order)'); $this->setDefaultSort('entity_id'); + $this->setFilterKeyPressCallback('order.productGridFilterKeyPress'); $this->setUseAjax(true); if ($this->getRequest()->getParam('collapse')) { $this->setIsCollapsed(true); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php index 06c6a9eb0652b..737ca446bb8e9 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; use Magento\Framework\Pricing\PriceCurrencyInterface; diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index f2200e1c1a108..a927b7177294a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Store\Model\ScopeInterface; /** * Adminhtml sales order create sidebar cart block @@ -146,4 +147,30 @@ private function getCartItemCustomPrice(Product $product): ?float return null; } + + /** + * @inheritdoc + */ + public function getItemCount() + { + $count = $this->getData('item_count'); + if ($count === null) { + $useQty = $this->_scopeConfig->getValue( + 'checkout/cart_link/use_qty', + ScopeInterface::SCOPE_STORE + ); + $allItems = $this->getItems(); + if ($useQty) { + $count = 0; + foreach ($allItems as $item) { + $count += $item->getQty(); + } + } else { + $count = count($allItems); + } + $this->setData('item_count', $count); + } + + return $count; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index 1210391f70ddb..50d29c195968c 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -111,20 +111,4 @@ public function getShippingLabel() } return $label; } - - /** - * Get update totals url. - * - * @return string - */ - public function getUpdateTotalsUrl(): string - { - return $this->getUrl( - 'sales/*/updateQty', - [ - 'order_id' => $this->getSource()->getOrderId(), - 'invoice_id' => $this->getRequest()->getParam('invoice_id', null), - ] - ); - } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php index 389c29bedf4c3..65163f9ed5d82 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php @@ -56,12 +56,7 @@ protected function _prepareLayout() $this->addChild( 'update_button', \Magento\Backend\Block\Widget\Button::class, - ['label' => __('Update Qty\'s'), 'class' => 'update-button secondary', 'onclick' => $onclick] - ); - $this->addChild( - 'update_totals_button', - \Magento\Backend\Block\Widget\Button::class, - ['label' => __('Update Totals'), 'class' => 'update-totals-button secondary', 'onclick' => $onclick] + ['label' => __('Update Qty\'s'), 'class' => 'update-button', 'onclick' => $onclick] ); if ($this->getCreditmemo()->canRefund()) { @@ -181,16 +176,6 @@ public function getUpdateButtonHtml() return $this->getChildHtml('update_button'); } - /** - * Get update totals button html - * - * @return string - */ - public function getUpdateTotalsButtonHtml(): string - { - return $this->getChildHtml('update_totals_button'); - } - /** * Get update url * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Totals.php index 8172a3c0db4ad..68843952035c8 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Totals.php @@ -5,7 +5,7 @@ */ namespace Magento\Sales\Block\Adminhtml; -use Magento\Sales\Model\Order; +use Magento\Framework\DataObject; /** * Adminhtml sales totals block @@ -57,60 +57,65 @@ public function formatValue($total) protected function _initTotals() { $this->_totals = []; - $this->_totals['subtotal'] = new \Magento\Framework\DataObject( + $order = $this->getSource(); + + $this->_totals['subtotal'] = new DataObject( [ 'code' => 'subtotal', - 'value' => $this->getSource()->getSubtotal(), - 'base_value' => $this->getSource()->getBaseSubtotal(), + 'value' => $order->getSubtotal(), + 'base_value' => $order->getBaseSubtotal(), 'label' => __('Subtotal'), ] ); /** - * Add shipping + * Add discount */ - if (!$this->getSource()->getIsVirtual() && ((double)$this->getSource()->getShippingAmount() || - $this->getSource()->getShippingDescription()) - ) { - $shippingLabel = __('Shipping & Handling'); - if ($this->isFreeShipping($this->getOrder()) && $this->getSource()->getDiscountDescription()) { - $shippingLabel .= sprintf(' (%s)', $this->getSource()->getDiscountDescription()); + if ((double)$order->getDiscountAmount() != 0) { + if ($order->getDiscountDescription()) { + $discountLabel = __('Discount (%1)', $order->getDiscountDescription()); + } else { + $discountLabel = __('Discount'); } - $this->_totals['shipping'] = new \Magento\Framework\DataObject( + $this->_totals['discount'] = new DataObject( [ - 'code' => 'shipping', - 'value' => $this->getSource()->getShippingAmount(), - 'base_value' => $this->getSource()->getBaseShippingAmount(), - 'label' => $shippingLabel, + 'code' => 'discount', + 'value' => $order->getDiscountAmount(), + 'base_value' => $order->getBaseDiscountAmount(), + 'label' => $discountLabel, ] ); } /** - * Add discount + * Add shipping */ - if ((double)$this->getSource()->getDiscountAmount() != 0) { - if ($this->getSource()->getDiscountDescription()) { - $discountLabel = __('Discount (%1)', $this->getSource()->getDiscountDescription()); - } else { - $discountLabel = __('Discount'); + if (!$order->getIsVirtual() + && ((double)$order->getShippingAmount() + || $order->getShippingDescription()) + ) { + $shippingLabel = __('Shipping & Handling'); + + if ($order->getCouponCode() && !isset($this->_totals['discount'])) { + $shippingLabel .= " ({$order->getCouponCode()})"; } - $this->_totals['discount'] = new \Magento\Framework\DataObject( + + $this->_totals['shipping'] = new DataObject( [ - 'code' => 'discount', - 'value' => $this->getSource()->getDiscountAmount(), - 'base_value' => $this->getSource()->getBaseDiscountAmount(), - 'label' => $discountLabel, + 'code' => 'shipping', + 'value' => $order->getShippingAmount(), + 'base_value' => $order->getBaseShippingAmount(), + 'label' => $shippingLabel, ] ); } - $this->_totals['grand_total'] = new \Magento\Framework\DataObject( + $this->_totals['grand_total'] = new DataObject( [ 'code' => 'grand_total', 'strong' => true, - 'value' => $this->getSource()->getGrandTotal(), - 'base_value' => $this->getSource()->getBaseGrandTotal(), + 'value' => $order->getGrandTotal(), + 'base_value' => $order->getBaseGrandTotal(), 'label' => __('Grand Total'), 'area' => 'footer', ] @@ -118,23 +123,4 @@ protected function _initTotals() return $this; } - - /** - * Availability of free shipping in at least one order item - * - * @param Order $order - * @return bool - */ - private function isFreeShipping(Order $order): bool - { - $isFreeShipping = false; - foreach ($order->getItems() as $orderItem) { - if ($orderItem->getFreeShipping() == '1') { - $isFreeShipping = true; - break; - } - } - - return $isFreeShipping; - } } diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index 83e66bbbce7cc..2e119d0bf887a 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -182,7 +182,7 @@ public function getFormatedOptionValue($optionValue) if ($this->string->strlen($optionValue) > 55) { $result['value'] = $result['value'] - . ' <a href="#" class="dots tooltip toggle" onclick="return false">...</a>'; + . ' ...'; $optionValue = nl2br($optionValue); $result = array_merge($result, ['full_view' => $optionValue]); } diff --git a/app/code/Magento/Sales/Block/Order/Totals.php b/app/code/Magento/Sales/Block/Order/Totals.php index 3720db76b5778..80ce5e2e689c6 100644 --- a/app/code/Magento/Sales/Block/Order/Totals.php +++ b/app/code/Magento/Sales/Block/Order/Totals.php @@ -8,6 +8,8 @@ use Magento\Sales\Model\Order; /** + * Order totals. + * * @api * @since 100.0.2 */ @@ -85,6 +87,8 @@ public function getOrder() } /** + * Sets order. + * * @param Order $order * @return $this */ @@ -118,20 +122,6 @@ protected function _initTotals() ['code' => 'subtotal', 'value' => $source->getSubtotal(), 'label' => __('Subtotal')] ); - /** - * Add shipping - */ - if (!$source->getIsVirtual() && ((double)$source->getShippingAmount() || $source->getShippingDescription())) { - $this->_totals['shipping'] = new \Magento\Framework\DataObject( - [ - 'code' => 'shipping', - 'field' => 'shipping_amount', - 'value' => $this->getSource()->getShippingAmount(), - 'label' => __('Shipping & Handling'), - ] - ); - } - /** * Add discount */ @@ -151,6 +141,25 @@ protected function _initTotals() ); } + /** + * Add shipping + */ + if (!$source->getIsVirtual() && ((double)$source->getShippingAmount() || $source->getShippingDescription())) { + $label = __('Shipping & Handling'); + if ($this->getSource()->getCouponCode() && !isset($this->_totals['discount'])) { + $label = __('Shipping & Handling (%1)', $this->getSource()->getCouponCode()); + } + + $this->_totals['shipping'] = new \Magento\Framework\DataObject( + [ + 'code' => 'shipping', + 'field' => 'shipping_amount', + 'value' => $this->getSource()->getShippingAmount(), + 'label' => $label, + ] + ); + } + $this->_totals['grand_total'] = new \Magento\Framework\DataObject( [ 'code' => 'grand_total', @@ -286,7 +295,6 @@ public function removeTotal($code) * $totalCode => $totalSortOrder * ) * - * * @param array $order * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -303,7 +311,7 @@ function ($code1, $code2) use ($order) { } /** - * get totals array for visualization + * Get totals array for visualization * * @param array|null $area * @return array diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php index 300b7ee37f2ef..6e7c2e5ce5609 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php @@ -1,17 +1,22 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice; use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\ForwardFactory; use Magento\Framework\App\ObjectManager; use Magento\Framework\Registry; use Magento\Sales\Api\InvoiceRepositoryInterface; -use Magento\Sales\Model\Order\InvoiceRepository; +/** + * Class View + */ abstract class View extends \Magento\Backend\App\Action { /** @@ -27,7 +32,7 @@ abstract class View extends \Magento\Backend\App\Action protected $registry; /** - * @var \Magento\Backend\Model\View\Result\ForwardFactory + * @var ForwardFactory */ protected $resultForwardFactory; @@ -39,16 +44,20 @@ abstract class View extends \Magento\Backend\App\Action /** * @param Context $context * @param Registry $registry - * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param ForwardFactory $resultForwardFactory + * @param InvoiceRepositoryInterface $invoiceRepository */ public function __construct( Context $context, Registry $registry, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + ForwardFactory $resultForwardFactory, + InvoiceRepositoryInterface $invoiceRepository = null ) { - $this->registry = $registry; parent::__construct($context); + $this->registry = $registry; $this->resultForwardFactory = $resultForwardFactory; + $this->invoiceRepository = $invoiceRepository ?: + ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); } /** @@ -70,13 +79,14 @@ public function execute() } /** + * Get invoice using invoice Id from request params + * * @return \Magento\Sales\Model\Order\Invoice|bool */ protected function getInvoice() { try { - $invoice = $this->getInvoiceRepository() - ->get($this->getRequest()->getParam('invoice_id')); + $invoice = $this->invoiceRepository->get($this->getRequest()->getParam('invoice_id')); $this->registry->register('current_invoice', $invoice); } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('Invoice capturing error')); @@ -85,19 +95,4 @@ protected function getInvoice() return $invoice; } - - /** - * @return InvoiceRepository - * - * @deprecated 100.1.0 - */ - private function getInvoiceRepository() - { - if ($this->invoiceRepository === null) { - $this->invoiceRepository = ObjectManager::getInstance() - ->get(InvoiceRepositoryInterface::class); - } - - return $this->invoiceRepository; - } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php index 341ee16ae910b..45cd504be201a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php @@ -188,7 +188,7 @@ protected function _processActionData($action = null) && $this->_getOrderCreateModel()->getShippingAddress()->getSameAsBilling() && empty($shippingMethod) ) { $this->_getOrderCreateModel()->setShippingAsBilling(1); - } else { + } elseif ($syncFlag !== null) { $this->_getOrderCreateModel()->setShippingAsBilling((int)$syncFlag); } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php index 515c0753542a0..23dcae3a858cc 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php @@ -1,23 +1,30 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\ForwardFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Api\InvoiceRepositoryInterface; use Magento\Sales\Model\Order\Email\Sender\InvoiceCommentSender; -use Magento\Sales\Model\Order\Invoice; -use Magento\Backend\App\Action; use Magento\Framework\Registry; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\Result\RawFactory; -class AddComment extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View +/** + * Class AddComment + */ +class AddComment extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View implements + HttpPostActionInterface { /** * @var InvoiceCommentSender @@ -39,29 +46,38 @@ class AddComment extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInv */ protected $resultRawFactory; + /** + * @var InvoiceRepositoryInterface + */ + protected $invoiceRepository; + /** * @param Context $context * @param Registry $registry - * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param ForwardFactory $resultForwardFactory * @param InvoiceCommentSender $invoiceCommentSender * @param JsonFactory $resultJsonFactory * @param PageFactory $resultPageFactory * @param RawFactory $resultRawFactory + * @param InvoiceRepositoryInterface $invoiceRepository */ public function __construct( Context $context, Registry $registry, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + ForwardFactory $resultForwardFactory, InvoiceCommentSender $invoiceCommentSender, JsonFactory $resultJsonFactory, PageFactory $resultPageFactory, - RawFactory $resultRawFactory + RawFactory $resultRawFactory, + InvoiceRepositoryInterface $invoiceRepository = null ) { $this->invoiceCommentSender = $invoiceCommentSender; $this->resultJsonFactory = $resultJsonFactory; $this->resultPageFactory = $resultPageFactory; $this->resultRawFactory = $resultRawFactory; - parent::__construct($context, $registry, $resultForwardFactory); + $this->invoiceRepository = $invoiceRepository ?: + ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); + parent::__construct($context, $registry, $resultForwardFactory, $invoiceRepository); } /** @@ -90,7 +106,7 @@ public function execute() ); $this->invoiceCommentSender->send($invoice, !empty($data['is_customer_notified']), $data['comment']); - $invoice->save(); + $this->invoiceRepository->save($invoice); /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php index 67a0dc469163b..f66ca37a47655 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -16,6 +16,7 @@ use Magento\Sales\Model\Order\ShipmentFactory; use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Helper\Data as SalesData; /** * Save invoice controller. @@ -56,6 +57,11 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac */ private $invoiceService; + /** + * @var SalesData + */ + private $salesData; + /** * @param Action\Context $context * @param Registry $registry @@ -63,6 +69,7 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac * @param ShipmentSender $shipmentSender * @param ShipmentFactory $shipmentFactory * @param InvoiceService $invoiceService + * @param SalesData $salesData */ public function __construct( Action\Context $context, @@ -70,7 +77,8 @@ public function __construct( InvoiceSender $invoiceSender, ShipmentSender $shipmentSender, ShipmentFactory $shipmentFactory, - InvoiceService $invoiceService + InvoiceService $invoiceService, + SalesData $salesData = null ) { $this->registry = $registry; $this->invoiceSender = $invoiceSender; @@ -78,6 +86,7 @@ public function __construct( $this->shipmentFactory = $shipmentFactory; $this->invoiceService = $invoiceService; parent::__construct($context); + $this->salesData = $salesData ?? $this->_objectManager->get(SalesData::class); } /** @@ -89,13 +98,18 @@ public function __construct( protected function _prepareShipment($invoice) { $invoiceData = $this->getRequest()->getParam('invoice'); - + $itemArr = []; + if (!isset($invoiceData['items']) || empty($invoiceData['items'])) { + $orderItems = $invoice->getOrder()->getItems(); + foreach ($orderItems as $item) { + $itemArr[$item->getId()] = (int)$item->getQtyOrdered(); + } + } $shipment = $this->shipmentFactory->create( $invoice->getOrder(), - isset($invoiceData['items']) ? $invoiceData['items'] : [], + isset($invoiceData['items']) ? $invoiceData['items'] : $itemArr, $this->getRequest()->getPost('tracking') ); - if (!$shipment->getTotalQty()) { return false; } @@ -199,7 +213,7 @@ public function execute() // send invoice/shipment emails try { - if (!empty($data['send_email'])) { + if (!empty($data['send_email']) || $this->salesData->canSendNewInvoiceEmail()) { $this->invoiceSender->send($invoice); } } catch (\Exception $e) { @@ -208,7 +222,7 @@ public function execute() } if ($shipment) { try { - if (!empty($data['send_email'])) { + if (!empty($data['send_email']) || $this->salesData->canSendNewShipmentEmail()) { $this->shipmentSender->send($shipment); } } catch (\Exception $e) { diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php index 021e7b66cd13f..a3242228b28e0 100644 --- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php +++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php @@ -5,40 +5,35 @@ */ namespace Magento\Sales\Cron; -use Magento\Store\Model\StoresConfig; +use Magento\Quote\Model\ResourceModel\Quote\Collection; +use Magento\Sales\Model\ResourceModel\Collection\ExpiredQuotesCollection; +use Magento\Store\Model\StoreManagerInterface; /** * Class CleanExpiredQuotes */ class CleanExpiredQuotes { - const LIFETIME = 86400; - - /** - * @var StoresConfig - */ - protected $storesConfig; - /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory + * @var ExpiredQuotesCollection */ - protected $quoteCollectionFactory; + private $expiredQuotesCollection; /** - * @var array + * @var StoreManagerInterface */ - protected $expireQuotesFilterFields = []; + private $storeManager; /** - * @param StoresConfig $storesConfig - * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $collectionFactory + * @param StoreManagerInterface $storeManager + * @param ExpiredQuotesCollection $expiredQuotesCollection */ public function __construct( - StoresConfig $storesConfig, - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $collectionFactory + StoreManagerInterface $storeManager, + ExpiredQuotesCollection $expiredQuotesCollection ) { - $this->storesConfig = $storesConfig; - $this->quoteCollectionFactory = $collectionFactory; + $this->storeManager = $storeManager; + $this->expiredQuotesCollection = $expiredQuotesCollection; } /** @@ -48,43 +43,11 @@ public function __construct( */ public function execute() { - $lifetimes = $this->storesConfig->getStoresConfigByPath('checkout/cart/delete_quote_after'); - foreach ($lifetimes as $storeId => $lifetime) { - $lifetime *= self::LIFETIME; - - /** @var $quotes \Magento\Quote\Model\ResourceModel\Quote\Collection */ - $quotes = $this->quoteCollectionFactory->create(); - - $quotes->addFieldToFilter('store_id', $storeId); - $quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]); - $quotes->addFieldToFilter('is_active', 0); - - foreach ($this->getExpireQuotesAdditionalFilterFields() as $field => $condition) { - $quotes->addFieldToFilter($field, $condition); - } - + $stores = $this->storeManager->getStores(true); + foreach ($stores as $store) { + /** @var $quotes Collection */ + $quotes = $this->expiredQuotesCollection->getExpiredQuotes($store); $quotes->walk('delete'); } } - - /** - * Retrieve expire quotes additional fields to filter - * - * @return array - */ - protected function getExpireQuotesAdditionalFilterFields() - { - return $this->expireQuotesFilterFields; - } - - /** - * Set expire quotes additional fields to filter - * - * @param array $fields - * @return void - */ - public function setExpireQuotesAdditionalFilterFields(array $fields) - { - $this->expireQuotesFilterFields = $fields; - } } diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 48deddb2fe5ac..89564f97ccf16 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -5,8 +5,11 @@ */ namespace Magento\Sales\Model; +use Magento\Config\Model\Config\Source\Nooptreq; use Magento\Directory\Model\Currency; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; @@ -14,6 +17,7 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; +use Magento\Sales\Api\OrderItemRepositoryInterface; use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Order\Address\Collection; @@ -24,8 +28,7 @@ use Magento\Sales\Model\ResourceModel\Order\Shipment\Collection as ShipmentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection as TrackCollection; use Magento\Sales\Model\ResourceModel\Order\Status\History\Collection as HistoryCollection; -use Magento\Sales\Api\OrderItemRepositoryInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Store\Model\ScopeInterface; /** * Order model @@ -299,6 +302,11 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ private $searchCriteriaBuilder; + /** + * @var ScopeConfigInterface; + */ + private $scopeConfig; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -331,6 +339,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param ProductOption|null $productOption * @param OrderItemRepositoryInterface $itemRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param ScopeConfigInterface $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -364,7 +373,8 @@ public function __construct( ResolverInterface $localeResolver = null, ProductOption $productOption = null, OrderItemRepositoryInterface $itemRepository = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null + SearchCriteriaBuilder $searchCriteriaBuilder = null, + ScopeConfigInterface $scopeConfig = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -392,6 +402,7 @@ public function __construct( ->get(OrderItemRepositoryInterface::class); $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() ->get(SearchCriteriaBuilder::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); parent::__construct( $context, @@ -1111,7 +1122,7 @@ public function addStatusHistoryComment($comment, $status = false) { return $this->addCommentToStatusHistory($comment, $status, false); } - + /** * Add a comment to order status history. * @@ -1503,7 +1514,7 @@ public function getItemById($itemId) * Get item by quote item id * * @param mixed $quoteItemId - * @return \Magento\Framework\DataObject|null + * @return \Magento\Framework\DataObject|null */ public function getItemByQuoteItemId($quoteItemId) { @@ -1967,11 +1978,23 @@ public function getRelatedObjects() */ public function getCustomerName() { - if ($this->getCustomerFirstname()) { - $customerName = $this->getCustomerFirstname() . ' ' . $this->getCustomerLastname(); - } else { - $customerName = (string)__('Guest'); + if (null === $this->getCustomerFirstname()) { + return (string)__('Guest'); } + + $customerName = ''; + if ($this->isVisibleCustomerPrefix() && strlen($this->getCustomerPrefix())) { + $customerName .= $this->getCustomerPrefix() . ' '; + } + $customerName .= $this->getCustomerFirstname(); + if ($this->isVisibleCustomerMiddlename() && strlen($this->getCustomerMiddlename())) { + $customerName .= ' ' . $this->getCustomerMiddlename(); + } + $customerName .= ' ' . $this->getCustomerLastname(); + if ($this->isVisibleCustomerSuffix() && strlen($this->getCustomerSuffix())) { + $customerName .= ' ' . $this->getCustomerSuffix(); + } + return $customerName; } @@ -4534,5 +4557,48 @@ public function setShippingMethod($shippingMethod) return $this->setData('shipping_method', $shippingMethod); } + /** + * Is visible customer middlename + * + * @return bool + */ + private function isVisibleCustomerMiddlename(): bool + { + return $this->scopeConfig->isSetFlag( + 'customer/address/middlename_show', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is visible customer prefix + * + * @return bool + */ + private function isVisibleCustomerPrefix(): bool + { + $prefixShowValue = $this->scopeConfig->getValue( + 'customer/address/prefix_show', + ScopeInterface::SCOPE_STORE + ); + + return $prefixShowValue !== Nooptreq::VALUE_NO; + } + + /** + * Is visible customer suffix + * + * @return bool + */ + private function isVisibleCustomerSuffix(): bool + { + $prefixShowValue = $this->scopeConfig->getValue( + 'customer/address/suffix_show', + ScopeInterface::SCOPE_STORE + ); + + return $prefixShowValue !== Nooptreq::VALUE_NO; + } + //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php index d4c2e7b2d6854..95dace13d832f 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -41,7 +41,7 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $baseOrderItemTax = (double)$orderItem->getBaseTaxInvoiced(); $orderItemQty = (double)$orderItem->getQtyInvoiced(); - if ($orderItemTax && $orderItemQty) { + if ($orderItemQty) { /** * Check item tax amount */ diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php index dc920ab62e0b7..f9efb9f56f504 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\CreditmemoCommentIdentity + */ class CreditmemoCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/creditmemo_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/creditmemo_comment/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/creditmemo_comment/identity'; @@ -15,6 +23,8 @@ class CreditmemoCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/creditmemo_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php index f60ef03800cf0..4c1fcfb501e3a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity + */ class CreditmemoIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/creditmemo/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/creditmemo/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/creditmemo/identity'; @@ -15,6 +23,8 @@ class CreditmemoIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/creditmemo/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php index 81584a61b7452..fd0a384a4bc6f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\InvoiceCommentIdentity + */ class InvoiceCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/invoice_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/invoice_comment/copy_to'; const XML_PATH_EMAIL_GUEST_TEMPLATE = 'sales_email/invoice_comment/guest_template'; @@ -15,6 +23,8 @@ class InvoiceCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/invoice_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php index db063b87271fb..6bb4eb0f0fd7f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity + */ class InvoiceIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/invoice/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/invoice/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/invoice/identity'; @@ -15,6 +23,8 @@ class InvoiceIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/invoice/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php index 3a79402913cc1..f43cd71ddd39a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\OrderCommentIdentity + */ class OrderCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/order_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/order_comment/copy_to'; const XML_PATH_EMAIL_GUEST_TEMPLATE = 'sales_email/order_comment/guest_template'; @@ -15,6 +23,8 @@ class OrderCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/order_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php index 68d269b4b4fc8..168869c67fb2d 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\OrderIdentity + */ class OrderIdentity extends Container implements IdentityInterface { /** @@ -18,6 +23,8 @@ class OrderIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/order/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -38,7 +45,7 @@ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php index 0c2f1d592dd19..db408ceecb4cc 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\ShipmentCommentIdentity + */ class ShipmentCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/shipment_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/shipment_comment/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/shipment_comment/identity'; @@ -15,6 +23,8 @@ class ShipmentCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/shipment_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php index ddf682d92fc87..d2f4c03f95b1f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity + */ class ShipmentIdentity extends Container implements IdentityInterface { /** @@ -41,7 +45,7 @@ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php index 09360d0685cf3..930791532539f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php @@ -73,6 +73,10 @@ public function send(Creditmemo $creditmemo, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php index 3cbd063641366..e6d528fb93a34 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php @@ -115,6 +115,12 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php index 32855f78c1571..9441f0e842925 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php @@ -73,6 +73,10 @@ public function send(Invoice $invoice, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 3ac5342de74a6..79133af6d6fb8 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -114,7 +114,13 @@ public function send(Invoice $invoice, $forceSyncMode = false) 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php index e162e01bd7555..4d37fc1b7769a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php @@ -70,6 +70,10 @@ public function send(Order $order, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php index bfbe1fb4fd7ff..c67804475cd65 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -130,6 +130,13 @@ protected function prepareTemplate(Order $order) 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'created_at_formatted' => $order->getCreatedAtFormatted(2), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php index b0b4907b96e70..ad305c8b7199f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php @@ -73,6 +73,10 @@ public function send(Shipment $shipment, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index df28dec701290..4dbc10308f3be 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -114,7 +114,13 @@ public function send(Shipment $shipment, $forceSyncMode = false) 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index c4523981ac729..ae188309ea646 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -85,8 +85,8 @@ public function sendCopyTo() $copyTo = $this->identityContainer->getEmailCopyTo(); if (!empty($copyTo)) { - $this->configureEmailTemplate(); foreach ($copyTo as $email) { + $this->configureEmailTemplate(); $this->transportBuilder->addTo($email); $transport = $this->transportBuilder->getTransport(); $transport->sendMessage(); diff --git a/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php b/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php index 5bcf579a1cbf4..24ccf45d60145 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Shipment; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; @@ -14,7 +16,13 @@ use Magento\Sales\Api\Data\ShipmentTrackSearchResultInterfaceFactory; use Magento\Sales\Api\ShipmentTrackRepositoryInterface; use Magento\Sales\Model\Spi\ShipmentTrackResourceInterface; +use Magento\Sales\Model\ResourceModel\Order\Shipment\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Psr\Log\LoggerInterface; +/** + * Repository of shipment tracking information + */ class TrackRepository implements ShipmentTrackRepositoryInterface { /** @@ -37,23 +45,40 @@ class TrackRepository implements ShipmentTrackRepositoryInterface */ private $collectionProcessor; + /** + * @var CollectionFactory + */ + private $shipmentCollection; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param ShipmentTrackResourceInterface $trackResource * @param ShipmentTrackInterfaceFactory $trackFactory * @param ShipmentTrackSearchResultInterfaceFactory $searchResultFactory * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory|null $shipmentCollection + * @param LoggerInterface|null $logger */ public function __construct( ShipmentTrackResourceInterface $trackResource, ShipmentTrackInterfaceFactory $trackFactory, ShipmentTrackSearchResultInterfaceFactory $searchResultFactory, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $shipmentCollection = null, + LoggerInterface $logger = null ) { - $this->trackResource = $trackResource; $this->trackFactory = $trackFactory; $this->searchResultFactory = $searchResultFactory; $this->collectionProcessor = $collectionProcessor; + $this->shipmentCollection = $shipmentCollection ?: + ObjectManager::getInstance()->get(CollectionFactory::class); + $this->logger = $logger ?: + ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -95,6 +120,16 @@ public function delete(ShipmentTrackInterface $entity) */ public function save(ShipmentTrackInterface $entity) { + $shipments = $this->shipmentCollection->create() + ->addFieldToFilter('order_id', $entity['order_id']) + ->addFieldToFilter('entity_id', $entity['parent_id']) + ->toArray(); + + if (empty($shipments['items'])) { + $this->logger->error('The shipment doesn\'t belong to the order.'); + throw new CouldNotSaveException(__('Could not save the shipment tracking.')); + } + try { $this->trackResource->save($entity); } catch (\Exception $e) { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Collection/ExpiredQuotesCollection.php b/app/code/Magento/Sales/Model/ResourceModel/Collection/ExpiredQuotesCollection.php new file mode 100644 index 0000000000000..895d73cc4cfff --- /dev/null +++ b/app/code/Magento/Sales/Model/ResourceModel/Collection/ExpiredQuotesCollection.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\ResourceModel\Collection; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Magento\Quote\Model\ResourceModel\Quote\Collection; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Class ExpiredQuotesCollection + */ +class ExpiredQuotesCollection +{ + /** + * @var int + */ + private $secondsInDay = 86400; + + /** + * @var string + */ + private $quoteLifetime = 'checkout/cart/delete_quote_after'; + + /** + * @var CollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @param ScopeConfigInterface $config + * @param CollectionFactory $collectionFactory + */ + public function __construct( + ScopeConfigInterface $config, + CollectionFactory $collectionFactory + ) { + $this->config = $config; + $this->quoteCollectionFactory = $collectionFactory; + } + + /** + * Gets expired quotes + * + * Quote is considered expired if the latest update date + * of the quote is greater than lifetime threshold + * + * @param StoreInterface $store + * @return AbstractCollection + */ + public function getExpiredQuotes(StoreInterface $store): AbstractCollection + { + $lifetime = $this->config->getValue( + $this->quoteLifetime, + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $lifetime *= $this->secondsInDay; + + /** @var $quotes Collection */ + $quotes = $this->quoteCollectionFactory->create(); + $quotes->addFieldToFilter('store_id', $store->getId()); + $quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]); + + return $quotes; + } +} diff --git a/app/code/Magento/Sales/Model/ResourceModel/Status/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Status/Collection.php index 23d835db603df..83346d4528c22 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Status/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Status/Collection.php @@ -1,12 +1,13 @@ <?php /** - * Oder statuses grid collection - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Sales\Model\ResourceModel\Status; +/** + * Order statuses grid collection. + */ class Collection extends \Magento\Sales\Model\ResourceModel\Order\Status\Collection { /** diff --git a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php index a698276332af8..a05ed2be9c82c 100644 --- a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php +++ b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php @@ -130,10 +130,12 @@ public function handle( foreach ($sendTo as $recipient) { $transport = $this->transportBuilder ->setTemplateIdentifier($template) - ->setTemplateOptions([ - 'area' => FrontNameResolver::AREA_CODE, - 'store' => Store::DEFAULT_STORE_ID, - ]) + ->setTemplateOptions( + [ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => Store::DEFAULT_STORE_ID, + ] + ) ->setTemplateVars($this->getTemplateVars($quote, $message, $checkoutType)) ->setFrom($this->getSendFrom($quote)) ->addTo($recipient['email'], $recipient['name']) @@ -170,6 +172,8 @@ private function getTemplateVars(Quote $quote, string $message, string $checkout 'customerEmail' => $quote->getBillingAddress()->getEmail(), 'billingAddress' => $quote->getBillingAddress(), 'shippingAddress' => $quote->getShippingAddress(), + 'billingAddressHtml' => $quote->getBillingAddress()->format('html'), + 'shippingAddressHtml' => $quote->getShippingAddress()->format('html'), 'shippingMethod' => $this->getConfigValue( 'carriers/' . $this->getShippingMethod($quote) . '/title', $quote diff --git a/app/code/Magento/Sales/Setup/Patch/Data/UpdateCreditmemoGridCurrencyCode.php b/app/code/Magento/Sales/Setup/Patch/Data/UpdateCreditmemoGridCurrencyCode.php deleted file mode 100644 index b143ae7c3ba7f..0000000000000 --- a/app/code/Magento/Sales/Setup/Patch/Data/UpdateCreditmemoGridCurrencyCode.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Sales\Setup\Patch\Data; - -use Magento\Framework\DB\Adapter\Pdo\Mysql; -use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Framework\Setup\Patch\DataPatchInterface; -use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\Sales\Setup\SalesSetup; -use Magento\Sales\Setup\SalesSetupFactory; - -/** - * Update credit memo grid currency code. - */ -class UpdateCreditmemoGridCurrencyCode implements DataPatchInterface, PatchVersionInterface -{ - /** - * @var ModuleDataSetupInterface - */ - private $moduleDataSetup; - - /** - * @var SalesSetupFactory - */ - private $salesSetupFactory; - - /** - * @param ModuleDataSetupInterface $moduleDataSetup - * @param SalesSetupFactory $salesSetupFactory - */ - public function __construct( - ModuleDataSetupInterface $moduleDataSetup, - SalesSetupFactory $salesSetupFactory - ) { - $this->moduleDataSetup = $moduleDataSetup; - $this->salesSetupFactory = $salesSetupFactory; - } - - /** - * @inheritdoc - */ - public function apply() - { - /** @var SalesSetup $salesSetup */ - $salesSetup = $this->salesSetupFactory->create(['setup' => $this->moduleDataSetup]); - /** @var Mysql $connection */ - $connection = $salesSetup->getConnection(); - $creditMemoGridTable = $salesSetup->getTable('sales_creditmemo_grid'); - $orderTable = $salesSetup->getTable('sales_order'); - $select = $connection->select(); - $condition = 'so.entity_id = scg.order_id'; - $select->join(['so' => $orderTable], $condition, ['order_currency_code', 'base_currency_code']); - $sql = $connection->updateFromSelect($select, ['scg' => $creditMemoGridTable]); - $connection->query($sql); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public static function getVersion() - { - return '2.0.13'; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml new file mode 100644 index 0000000000000..4fd992418887a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.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="AdminAssertProductInShoppingCartSectionActionGroup"> + <annotations> + <description>Assert product in Shopping cart section in Customer's Activities block on Create Order Page.</description> + </annotations> + <arguments> + <argument name="product" type="string"/> + </arguments> + + <see selector="{{AdminCreateOrderShoppingCartSection.shoppingCartBlock}}" userInput="{{product}}" stepKey="seeProductInShoppingCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertRefundOrderStatusInCommentsHistoryActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertRefundOrderStatusInCommentsHistoryActionGroup.xml index aefc8abc3f05c..e5b581e14d8b0 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertRefundOrderStatusInCommentsHistoryActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertRefundOrderStatusInCommentsHistoryActionGroup.xml @@ -20,7 +20,7 @@ <!-- Assert refund order status in Comments History --> <click selector="{{AdminOrderDetailsOrderViewSection.commentsHistory}}" stepKey="clickOnTabCommentsHistory"/> <waitForPageLoad stepKey="waitForComments"/> - <see userInput="{{orderStatus}}" selector="{{ViewOrderSection.orderStatus}}" stepKey="assertRefundOrderStatusInCommentsHistory"/> - <see userInput="{{refundMessage}}" selector="{{ViewOrderSection.capturedAmountTextUnsubmitted}}" stepKey="assertOrderStatus"/> + <see userInput="{{orderStatus}}" selector="{{AdminOrderCommentsTabSection.orderNotesList}}" stepKey="assertRefundOrderStatusInCommentsHistory"/> + <see userInput="{{refundMessage}}" selector="{{AdminOrderCommentsTabSection.orderComment}}" stepKey="assertOrderStatus"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml index 68cd1c42e1dd8..75d41d835bed2 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml @@ -19,7 +19,7 @@ <argument name="billingAddress" defaultValue=""/> <argument name="customerGroup" defaultValue="GeneralCustomerGroup"/> </arguments> - + <see selector="{{AdminCreditMemoOrderInformationSection.customerName}}" userInput="{{customer.firstname}}" stepKey="seeCustomerName"/> <see selector="{{AdminCreditMemoOrderInformationSection.customerEmail}}" userInput="{{customer.email}}" stepKey="seeCustomerEmail"/> <see selector="{{AdminCreditMemoOrderInformationSection.customerGroup}}" userInput="{{customerGroup.code}}" stepKey="seeCustomerGroup"/> @@ -42,7 +42,7 @@ <arguments> <argument name="product"/> </arguments> - + <see selector="{{AdminCreditMemoItemsSection.skuColumn}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> </actionGroup> @@ -59,8 +59,8 @@ <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="waitButtonEnabled"/> <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemo"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You created the credit memo." stepKey="seeCreditMemoCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." stepKey="seeCreditMemoCreateSuccess"/> <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}$grabOrderId" stepKey="seeViewOrderPageCreditMemo"/> </actionGroup> <actionGroup name="UpdateCreditMemoTotalsActionGroup"> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml index 03639546631d1..8451f9de03293 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml @@ -99,8 +99,8 @@ </annotations> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageInvoice"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..8512387d45e8a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminMoveProductToItemsOrderedFromShoppingCartActionGroup"> + <annotations> + <description>Move product to the "Items Ordered" section from shopping cart.</description> + </annotations> + <arguments> + <argument name="product" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminCreateOrderShoppingCartSection.addToOrderCheckBox(product)}}" stepKey="waitForAddToOrderCheckBox"/> + <click selector="{{AdminCreateOrderShoppingCartSection.addToOrderCheckBox(product)}}" stepKey="selectProduct"/> + <click selector="{{AdminCustomerCreateNewOrderSection.updateChangesBtn}}" stepKey="clickOnUpdateButton"/> + <waitForPageLoad stepKey="waitForAdminCreateOrderShoppingCartSectionPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundActionGroup.xml index 60134ee7e039c..da4c80bb28586 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundActionGroup.xml @@ -34,8 +34,9 @@ <fillField userInput="{{shippingRefund}}" selector="{{AdminCreditMemoTotalSection.refundShipping}}" stepKey="fillShipping"/> <fillField userInput="{{adjustmentRefund}}" selector="{{AdminCreditMemoTotalSection.adjustmentRefund}}" stepKey="fillAdjustmentRefund"/> <fillField userInput="{{adjustmentFee}}" selector="{{AdminCreditMemoTotalSection.adjustmentFee}}" stepKey="fillAdjustmentFee"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="waitForUpdateTotalsButton"/> + <click selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="clickUpdateTotals"/> <checkOption selector="{{AdminCreditMemoTotalSection.emailCopy}}" stepKey="checkSendEmailCopy"/> - <conditionalClick selector="{{AdminCreditMemoTotalSection.updateTotals}}" dependentSelector="{{AdminCreditMemoTotalSection.disabledUpdateTotals}}" visible="false" stepKey="clickUpdateTotalsButton"/> </actionGroup> <!-- Open and fill CreditMemo refund with back to stock --> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 3f178ae02102a..90e2aa8e12527 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -92,7 +92,7 @@ <annotations> <description>Clears the Email, First Name, Last Name, Street Line 1, City, Postal Code and Phone fields when adding an Order and then verifies that they are required after attempting to Save.</description> </annotations> - + <seeElement selector="{{AdminOrderFormAccountSection.requiredGroup}}" stepKey="seeCustomerGroupRequired"/> <seeElement selector="{{AdminOrderFormAccountSection.requiredEmail}}" stepKey="seeEmailRequired"/> <clearField selector="{{AdminOrderFormAccountSection.email}}" stepKey="clearEmailField"/> @@ -181,7 +181,7 @@ <annotations> <description>EXTENDS: addConfigurableProductToOrder. Selects the provided Option to the Configurable Product.</description> </annotations> - + <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_frontend_label)}}" stepKey="waitForConfigurablePopover"/> <selectOption selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_frontend_label)}}" userInput="{{option.label}}" stepKey="selectionConfigurableOption"/> </actionGroup> @@ -195,7 +195,7 @@ <argument name="option"/> <argument name="quantity" type="string"/> </arguments> - + <click selector="{{AdminOrderFormItemsSection.configure}}" stepKey="clickConfigure"/> <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_frontend_label)}}" stepKey="waitForConfigurablePopover"/> <wait time="2" stepKey="waitForOptionsToLoad"/> @@ -213,7 +213,7 @@ <argument name="product"/> <argument name="quantity" type="string" defaultValue="1"/> </arguments> - + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterBundle"/> <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> @@ -235,7 +235,7 @@ <arguments> <argument name="price" type="string"/> </arguments> - + <grabTextFrom selector="{{AdminOrderFormItemsSection.rowPrice('1')}}" stepKey="grabProductPriceFromGrid" after="clickOk"/> <assertEquals stepKey="assertProductPriceInGrid" message="Bundle product price in grid should be equal {{price}}" after="grabProductPriceFromGrid"> <expectedResult type="string">{{price}}</expectedResult> @@ -320,6 +320,22 @@ <selectOption selector="{{AdminOrderFormPaymentSection.flatRateOption}}" userInput="flatrate_flatrate" stepKey="checkFlatRate"/> </actionGroup> + <actionGroup name="changeShippingMethod"> + <annotations> + <description>Change Shipping Method on the Admin 'Create New Order for' page.</description> + </annotations> + <arguments> + <argument name="shippingMethod" defaultValue="flatrate_flatrate" type="string"/> + </arguments> + <click selector="{{AdminOrderFormPaymentSection.header}}" stepKey="unfocus"/> + <waitForPageLoad stepKey="waitForJavascriptToFinish"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShippingMethods1"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="waitForChangeShippingMethod"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShippingMethods2"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.shippingMethod}}" stepKey="waitForShippingOptions2"/> + <selectOption selector="{{AdminOrderFormPaymentSection.shippingMethod}}" userInput="{{shippingMethod}}" stepKey="checkFlatRate"/> + </actionGroup> + <!--Select free shipping method--> <actionGroup name="orderSelectFreeShipping"> <annotations> @@ -516,7 +532,7 @@ <argument name="product"/> <argument name="customer"/> </arguments> - + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> <waitForPageLoad stepKey="waitForNewOrderPageOpened"/> <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderSelectShippingMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderSelectShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..b8493bf288378 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderSelectShippingMethodActionGroup.xml @@ -0,0 +1,26 @@ +<?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="AdminOrderSelectShippingMethodActionGroup"> + <annotations> + <description>Select Shipping method from admin order page.</description> + </annotations> + <arguments> + <argument name="methodTitle" type="string" defaultValue="flatrate"/> + <argument name="methodName" type="string" defaultValue="fixed"/> + </arguments> + <waitForElementVisible selector="{{AdminInvoicePaymentShippingSection.getShippingMethod}}" stepKey="waitForShippingMethodsOpen"/> + <click selector="{{AdminInvoicePaymentShippingSection.getShippingMethod}}" stepKey="openShippingMethod"/> + <conditionalClick selector="{{AdminInvoicePaymentShippingSection.getShippingMethod}}" dependentSelector="{{AdminInvoicePaymentShippingSection.fixedPriceShippingMethod(methodTitle, methodName)}}" visible="false" stepKey="openShippingMethodSecondTime"/> + <waitForElementVisible selector="{{AdminInvoicePaymentShippingSection.fixedPriceShippingMethod(methodName, methodTitle)}}" stepKey="waitForShippingMethod"/> + <click selector="{{AdminInvoicePaymentShippingSection.fixedPriceShippingMethod(methodName, methodTitle)}}" stepKey="chooseShippingMethod"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertAdminShippingDescriptionInOrderViewActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertAdminShippingDescriptionInOrderViewActionGroup.xml new file mode 100644 index 0000000000000..1e4c0a958b2e4 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertAdminShippingDescriptionInOrderViewActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertAdminShippingDescriptionInOrderViewActionGroup"> + <annotations> + <description>Validates that the Shipping Description will shown in Shipping total description.</description> + </annotations> + + <arguments> + <argument name="description" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminOrderTotalSection.shippingDescription}}" time="30" stepKey="waitForElement"/> + <see selector="{{AdminOrderTotalSection.shippingDescription}}" userInput="{{description}}" stepKey="seeOrderTotalShippingDescription"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertStorefrontShippingDescriptionInOrderViewActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertStorefrontShippingDescriptionInOrderViewActionGroup.xml new file mode 100644 index 0000000000000..27e91883eb15c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertStorefrontShippingDescriptionInOrderViewActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertStorefrontShippingDescriptionInOrderViewActionGroup"> + <annotations> + <description>Validates that the Shipping Description will shown in Shipping total description.</description> + </annotations> + + <arguments> + <argument name="description" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontOrderDetailsSection.shippingTotalDescription}}" time="30" stepKey="waitForElement"/> + <see selector="{{StorefrontOrderDetailsSection.shippingTotalDescription}}" userInput="{{description}}" stepKey="seeShippingTotalDescription"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml index aaeb9ffb30bd9..7388eaa96f215 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="Category"/> </arguments> - + <amOnPage url="{{StorefrontCategoryPage.url(Category.name)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml new file mode 100644 index 0000000000000..03a14ca514091 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontCustomerReorderActionGroup"> + <annotations> + <description>Navigate to customer dashboard -> orders. Press 'reorder' button for specified order id. Notice: customer should be logged in.</description> + </annotations> + <arguments> + <argument name="orderNumber" type="string"/> + </arguments> + <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="goToCustomerDashboardPage"/> + <waitForPageLoad stepKey="waitForCustomerDashboardPageLoad"/> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab('My Orders')}}" stepKey="navigateToOrders"/> + <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <click selector="{{StorefrontCustomerOrdersGridSection.reorderBtn(orderNumber)}}" stepKey="clickReorderBtn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml index bc9486d61fbfe..680d44ebb34fe 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml @@ -19,5 +19,6 @@ <section name="AdminOrderFormTotalSection"/> <section name="AdminOrderFormStoreSelectorSection"/> <section name="AdminOrderFormDiscountSection"/> + <section name="AdminOrderFormMessagesSection"/> </page> </pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml index fda886a839802..32e987bea919b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml @@ -17,5 +17,7 @@ <element name="CreateShipment" type="checkbox" selector=".order-shipping-address input[name='invoice[do_shipment]']"/> <element name="getShippingMethodAndRates" type="button" selector="//span[text()='Get shipping methods and rates']" timeout="60"/> <element name="shippingMethod" type="button" selector="//label[contains(text(), 'Fixed')]" timeout="60"/> + <element name="fixedPriceShippingMethod" type="button" selector="#s_method_{{methodName}}_{{methodTitle}}" parameterized="true"/> + <element name="getShippingMethod" type="button" selector="#order-shipping-method-summary a"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsTabSection.xml index 19f447117959a..dce0875e29336 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsTabSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsTabSection.xml @@ -11,5 +11,6 @@ <section name="AdminOrderCommentsTabSection"> <element name="orderNotesList" type="text" selector="#Order_History .edit-order-comments .note-list"/> <element name="orderComments" type="text" selector="#Order_History .edit-order-comments-block"/> + <element name="orderComment" type="text" selector="#Order_History .comments-block-item-comment"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml index 71a96dc109385..653b1d48686e3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml @@ -13,6 +13,8 @@ <element name="orderStatus" type="text" selector=".order-information table.order-information-table #order_status"/> <element name="purchasedFrom" type="text" selector=".order-information table.order-information-table tr:last-of-type > td"/> <element name="accountInformation" type="text" selector=".order-account-information-table"/> + <element name="orderInformationTable" type="block" selector=".order-information-table"/> + <element name="rate" type="text" selector="//table[contains(@class, 'order-information-table')]//th[contains(text(), 'rate:')]"/> <element name="customerName" type="text" selector=".order-account-information table tr:first-of-type > td span"/> <element name="customerEmail" type="text" selector=".order-account-information table tr:nth-of-type(2) > td a"/> <element name="customerGroup" type="text" selector=".order-account-information table tr:nth-of-type(3) > td"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml index 2d1a4d5a4cbae..4fde9db1d21d8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormBillingAddressSection"> + <element name="selectAddress" type="select" selector="//select[@id='order-billing_address_customer_address_id']"/> <element name="NamePrefix" type="input" selector="#order-billing_address_prefix" timeout="30"/> <element name="FirstName" type="input" selector="#order-billing_address_firstname" timeout="30"/> <element name="MiddleName" type="input" selector="#order-billing_address_middlename" timeout="30"/> @@ -38,4 +39,4 @@ <element name="postalCodeError" type="text" selector="#order-billing_address_postcode-error"/> <element name="phoneError" type="text" selector="#order-billing_address_telephone-error"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml index 8c093891ac46d..4f065ec7eb748 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormConfigureProductSection"> + <element name="configure" type="button" selector="//a[@product_id='{{productId}}']" parameterized="true"/> <element name="optionSelect" type="select" selector="//div[contains(@class,'product-options')]//select[//label[text() = '{{option}}']]" parameterized="true"/> <element name="optionSelectNew" type="select" selector="//label[text()='{{option1}}']/following-sibling::div/select" parameterized="true"/> <element name="quantity" type="input" selector="#product_composite_configure_input_qty"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormMessagesSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormMessagesSection.xml new file mode 100644 index 0000000000000..b5e6f6b6ede83 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormMessagesSection.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="AdminOrderFormMessagesSection"> + <element name="success" type="text" selector="#order-message div.message-success"/> + <element name="error" type="text" selector="#order-message div.message-error"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index b31582552cccc..a478d79d8553f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormPaymentSection"> + <element name="shippingMethod" type="radio" selector="//input[@name='order[shipping_method]']"/> <element name="header" type="text" selector="#order-methods span.title"/> <element name="getShippingMethods" type="text" selector="#order-shipping_method a.action-default" timeout="30"/> <element name="flatRateOption" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml index 6f62ce199ecbb..f57b1f65eb94e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml @@ -11,6 +11,6 @@ <section name="AdminOrderFormTotalSection"> <element name="subtotalRow" type="text" selector="#order-totals>table tr.row-totals:nth-of-type({{row}}) span.price" parameterized="true"/> <element name="total" type="text" selector="//tr[contains(@class,'row-totals')]/td[contains(text(), '{{total}}')]/following-sibling::td/span[contains(@class, 'price')]" parameterized="true"/> - <element name="grandTotal" type="text" selector="#order-totals>table tr.row-totals:nth-of-type(3) span.price"/> + <element name="grandTotal" type="text" selector="//tr[contains(@class,'row-totals')]/td/strong[contains(text(), 'Grand Total')]/parent::td/following-sibling::td//span[contains(@class, 'price')]"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml index 88d90bc716576..83eb733c8d298 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml @@ -17,7 +17,7 @@ <element name="filters" type="button" selector="//div[@id='sales_order_view_tabs_order_invoices_content']//button[@data-action='grid-filter-expand']" timeout="30"/> <element name="applyFilters" type="button" selector="//div[@id='sales_order_view_tabs_order_invoices_content']//button[@data-action='grid-filter-apply']" timeout="30"/> <element name="invoiceId" type="input" selector="//div[@id='sales_order_view_tabs_order_invoices_content']//input[@name='increment_id']" timeout="30"/> - <element name="amountFrom" type="input" selector="[name='base_grand_total[from]']" timeout="30"/> - <element name="amountTo" type="input" selector="[name='base_grand_total[to]']" timeout="30"/> + <element name="amountFrom" type="input" selector="[name='grand_total[from]']" timeout="30"/> + <element name="amountTo" type="input" selector="[name='grand_total[to]']" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml index 9b7356127df69..eb9c32d6ced9f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml @@ -11,6 +11,7 @@ <section name="AdminOrderTotalSection"> <element name="subTotal" type="text" selector=".order-subtotal-table tbody tr.col-0>td span.price"/> <element name="grandTotal" type="text" selector=".order-subtotal-table tfoot tr.col-0>td span.price"/> + <element name="shippingDescription" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[contains(text(), 'Shipping & Handling')]"/> <element name="shippingAndHandling" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[normalize-space(.)='Shipping & Handling']/following-sibling::td//span[@class='price']"/> <element name="total" type="text" selector="//table[contains(@class,'order-subtotal-table')]/tbody/tr/td[contains(text(), '{{total}}')]/following-sibling::td/span/span[contains(@class, 'price')]" parameterized="true"/> </section> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index ace64cdaa1032..a18ca0c415567 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -39,5 +39,6 @@ <element name="changeOrderStatus" type="button" selector="//div[contains(concat(' ',normalize-space(@class),' '),' row-gutter ')]//span[text()='{{status}}']" parameterized="true" timeout="30"/> <element name="viewLink" type="text" selector="//td/div[contains(.,'{{orderID}}')]/../..//a[@class='action-menu-item']" parameterized="true"/> <element name="selectOrderID" type="checkbox" selector="//td/div[text()='{{orderId}}']/../preceding-sibling::td//input" parameterized="true" timeout="60"/> + <element name="orderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[contains(text(), '{{orderId}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml index 415bac7fd051d..c0deb9ab55d2b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerOrdersGridSection"> <element name="orderView" type="button" selector="//td[text()='{{orderNumber}}']/following-sibling::td[@class='col actions']/a[contains(@class, 'view')]" parameterized="true" /> + <element name="reorderBtn" type="button" selector="//td[text()='{{orderNumber}}']/following-sibling::td[@class='col actions']/a[contains(@class, 'order')]" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml index c255e15cd6ecf..d262dfa9b010c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml @@ -12,6 +12,7 @@ <element name="orderDetailsBlock" type="block" selector=".block-order-details-view"/> <element name="billingAddressBlock" type="block" selector=".box-order-billing-address > .box-content > address"/> <element name="discountSalesRule" type="text" selector="tr.discount span.price"/> + <element name="shippingTotalDescription" type="text" selector="#my-orders-table tr.shipping th.mark"/> <element name="grandTotalPrice" type="text" selector="tr.grand_total span.price"/> <element name="paymentMethod" type="text" selector=".box-order-billing-method dt.title"/> <element name="shippingMethod" type="text" selector=".box-order-shipping-method div.box-content"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml index 76be3a1094327..589da3e49dc89 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml @@ -46,8 +46,9 @@ </actionGroup> <!-- Add product to cart --> - <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <actionGroup ref="StorefrontAddSimpleProductWithQtyActionGroup" stepKey="addSimpleProductToCart"> <argument name="product" value="$$createProduct$$"/> + <argument name="quantity" value="2"/> </actionGroup> <!-- Login as admin --> @@ -64,6 +65,7 @@ <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> <!-- Check product in customer's activities in shopping cart section --> + <see selector="{{AdminCreateOrderShoppingCartSection.shoppingCartBlock}}" userInput="Shopping Cart (2)" stepKey="seeCorrectNumberInCart"/> <see selector="{{AdminCustomerActivitiesShoppingCartSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInShoppingCartSection"/> <see selector="{{AdminCustomerActivitiesShoppingCartSection.productPrice}}" userInput="$$createProduct.price$$" stepKey="seeProductPriceInShoppingCartSection"/> @@ -75,6 +77,6 @@ <!-- Assert product in items ordered grid --> <see selector="{{AdminCustomerCreateNewOrderSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeProductName"/> <see selector="{{AdminCustomerCreateNewOrderSection.productPrice}}" userInput="$$createProduct.price$$" stepKey="seeProductPrice"/> - <seeInField selector="{{AdminCustomerCreateNewOrderSection.productQty}}" userInput="{{ApiSimpleSingleQty.quantity}}" stepKey="seeProductQty"/> + <seeInField selector="{{AdminCustomerCreateNewOrderSection.productQty}}" userInput="2" stepKey="seeProductQty"/> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml index 1ad736ade37fc..4310d412d1c98 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -55,6 +55,9 @@ <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Go to bundle product page--> <amOnPage url="{{StorefrontProductPage.url($$createCategory.name$$)}}" stepKey="navigateToBundleProductPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml index f79ab822d964a..37a9b97fab064 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml @@ -64,8 +64,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startInvoice"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> <!-- Go to Sales > Orders > find out placed order and open --> <grabTextFrom selector="|Order # (\d+)|" stepKey="grabOrderId" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml index 0522960a032fa..8a9369537f0a4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml @@ -123,8 +123,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startInvoice"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> <!-- Go to Sales > Orders > find out placed order and open --> <grabTextFrom selector="|Order # (\d+)|" stepKey="grabOrderId" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml index e9b37521259a0..418c0e72dc1fc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml @@ -59,8 +59,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startInvoice"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> <!-- Go to Sales > Orders > find out placed order and open --> <grabTextFrom selector="|Order # (\d+)|" stepKey="grabOrderId" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml index 791792d0879a7..c552f93e62a4a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml @@ -65,8 +65,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startInvoice"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> <!-- Go to Sales > Orders > find out placed order and open --> <grabTextFrom selector="|Order # (\d+)|" stepKey="grabOrderId" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml index 0a8e78d743c1d..57d9222d85096 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml @@ -68,8 +68,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startInvoice"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> <!-- Go to Sales > Orders > find out placed order and open --> <grabTextFrom selector="|Order # (\d+)|" stepKey="grabOrderId" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index 800517236cb39..a90fe5c49f032 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -25,7 +25,7 @@ </createData> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct" stepKey="deleteCategory1"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml new file mode 100644 index 0000000000000..bd13f7c847c34 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.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="AdminCreateOrderAddProductCheckboxTest"> + <annotations> + <title value="Create Order in Admin and Add Product"/> + <stories value="Create order and add product using checkbox"/> + <description value="Create order in Admin panel, add product by clicking checkbox, and verify it is checked"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + + <before> + <!-- Create simple customer --> + <createData entity="Simple_US_Customer_CA" stepKey="createSimpleCustomer"/> + + <!-- Create simple product --> + <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> + + <!-- Login to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Initiate create new order --> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createSimpleCustomer$$"/> + </actionGroup> + + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSkuFilterBundle"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> + <seeCheckboxIsChecked selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="verifyProductChecked"/> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml new file mode 100644 index 0000000000000..80697fd57736a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml @@ -0,0 +1,61 @@ +<?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="AdminCreateOrderAndCheckTheReorderTest"> + <annotations> + <title value="'Reorder' button is not visible for customer if ordered item is out of stock"/> + <stories value="MAGETWO-63924: 'Reorder' button is not visible for customer if ordered item is out of stock"/> + <description value="'Reorder' button is not visible for customer if ordered item is out of stock"/> + <features value="Sales"/> + <testCaseId value="MC-22109"/> + <severity value="MAJOR"/> + <group value="Sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + <createData entity="CashOnDeliveryPaymentMethodDefault" stepKey="cashOnDeliveryPaymentMethod"/> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> + <createData entity="SimpleProduct_25" stepKey="simpleProduct"> + <field key="price">5</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$simpleProduct$$"/> + <argument name="productQty" value="{{SimpleProduct_25.quantity}}"/> + </actionGroup> + <actionGroup ref="SelectCashOnDeliveryPaymentMethodActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> + <actionGroup ref="OpenOrderById" stepKey="openOrder"> + <argument name="orderId" value="$getOrderId"/> + </actionGroup> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="frontendCustomerLogIn"> + <argument name="Customer" value="$$simpleCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontNavigateToCustomerOrdersHistoryPageActionGroup" stepKey="goToOrderHistoryPage"/> + <actionGroup ref="StorefrontCustomerReorderButtonNotVisibleActionGroup" stepKey="checkReorderButton"/> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> + <magentoCLI command="config:set payment/cashondelivery/active 0" stepKey="disableCashOnDeliveryMethod"/> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml new file mode 100644 index 0000000000000..d6bf0eec301db --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml @@ -0,0 +1,75 @@ +<?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="AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest"> + <annotations> + <title value="Tax should not be displayed for non taxable address"/> + <stories value="MC-21699: Tax does not change when changing the billing address from Admin Panel"/> + <description value="Tax should not be displayed for non taxable address when switching from taxable address"/> + <testCaseId value="MC-21721"/> + <features value="Sales"/> + <severity value="MAJOR"/> + <group value="Sales"/> + </annotations> + <before> + <!--Enable flat rate shipping--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!--Enable free shipping method --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + <!--Create customer--> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="simpleCustomer"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="category1"/> + <!--Create product1--> + <createData entity="_defaultProduct" stepKey="product1"> + <requiredEntity createDataKey="category1"/> + </createData> + <!--Create tax rule for US-CA--> + <createData entity="defaultTaxRule" stepKey="createTaxRule"/> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <!--Step 1: Create new order for customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + <!--Step 2: Add product1 to the order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <!--Step 2: Select taxable address as billing address--> + <selectOption selector="{{AdminOrderFormBillingAddressSection.selectAddress}}" userInput="{{US_Address_CA.state}}" stepKey="selectTaxableAddress" /> + <!--Step 3: Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShippingMethod"/> + <!--Step 4: Verify that tax is applied to the order--> + <seeElement selector="{{AdminOrderFormTotalSection.total('Tax')}}" stepKey="seeTax" /> + <!--Step 5: Select non taxable address as billing address--> + <selectOption selector="{{AdminOrderFormBillingAddressSection.selectAddress}}" userInput="{{US_Address_TX.state}}" stepKey="selectNonTaxableAddress" /> + <!--Step 6: Change shipping method to Free--> + <actionGroup ref="changeShippingMethod" stepKey="changeShippingMethod"> + <argument name="shippingMethod" value="freeshipping_freeshipping"/> + </actionGroup> + <!--Step 7: Verify that tax is not applied to the order--> + <dontSeeElement selector="{{AdminOrderFormTotalSection.total('Tax')}}" stepKey="dontSeeTax" /> + <after> + <!--Delete product1--> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <!--Delete category--> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <!--Delete customer--> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <!--Delete tax rule--> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logout"/> + <!--Disable free shipping method --> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml new file mode 100644 index 0000000000000..85567374e36e4 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml @@ -0,0 +1,59 @@ +<?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="AdminCreateOrderWithSimpleProductTest"> + <annotations> + <title value="Create Order in Admin with simple product"/> + <stories value="MAGETWO-12798: Create order with condition available product qty = ordered product qty"/> + <description value="Create order with simple product and assert if it gets out of stock after ordering it."/> + <features value="Sales"/> + <testCaseId value="MC-22110"/> + <severity value="MAJOR"/> + <group value="Sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + <createData entity="CashOnDeliveryPaymentMethodDefault" stepKey="cashOnDeliveryPaymentMethod"/> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> + <createData entity="SimpleProduct_25" stepKey="simpleProduct"> + <field key="price">5</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$simpleProduct$$"/> + <argument name="productQty" value="{{SimpleProduct_25.quantity}}"/> + </actionGroup> + <actionGroup ref="SelectCashOnDeliveryPaymentMethodActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> + <actionGroup ref="OpenOrderById" stepKey="openOrder"> + <argument name="orderId" value="$getOrderId"/> + </actionGroup> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductStockStatus"> + <argument name="productId" value="$$simpleProduct.id$$"/> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <after> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="config:set payment/cashondelivery/active 0" stepKey="disableCashOnDeliveryMethod"/> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml index 5f6ea0937b52a..8bfcaf67c4332 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -57,7 +57,7 @@ <!--Submit Order and verify that Order isn't placed--> <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> - <dontSeeElement selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> - <seeElement selector="{{AdminMessagesSection.errorMessage}}" stepKey="seeErrorMessage"/> + <dontSeeElement selector="{{AdminOrderFormMessagesSection.success}}" stepKey="seeSuccessMessage"/> + <seeElement selector="{{AdminOrderFormMessagesSection.error}}" stepKey="seeErrorMessage"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml new file mode 100644 index 0000000000000..b40e9d041a10e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml @@ -0,0 +1,107 @@ +<?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="AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest"> + <annotations> + <stories value="Admin create order"/> + <title value="Product in the shopping cart could be reached by admin during order creation with multi website config"/> + <description value="Product in the shopping cart could be reached by admin during order creation with multi website config"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6353"/> + <group value="sales"/> + <skip> + <issueId value="MC-20129"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="goToProductPageViaID" stepKey="goToProductEditPage"> + <argument name="productId" value="$$createProduct.id$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="assignProductToSecondWebsite"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create customer account for Second Website--> + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageUsingStoreCodeInUrlActionGroup" stepKey="goToCreateCustomerPage"/> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="Thank you for registering with {{customStoreGroup.name}}." /> + </actionGroup> + + <!--Open product page and add to cart--> + <actionGroup ref="StorefrontOpenProductPageUsingStoreCodeInUrlActionGroup" stepKey="openProductPageUsingStoreCodeInUrl"> + <argument name="product" value="$$createProduct$$"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="addProductToCart"/> + + <!--Create new order for existing Customer And Store--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrder"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <!--Assert product in Shopping cart section--> + <actionGroup ref="AdminAssertProductInShoppingCartSectionActionGroup" stepKey="seeProductInShoppingCart"> + <argument name="product" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Move product to the order from shopping cart--> + <actionGroup ref="AdminMoveProductToItemsOrderedFromShoppingCartActionGroup" stepKey="addProductToItemsOrderedFromShoppingCart"> + <argument name="product" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!--Select shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + + <!--Checkout select Check/Money Order payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + + <!--Submit Order and verify information--> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml index 587b23e857c0c..bddc114f5dd5e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml @@ -18,9 +18,6 @@ <useCaseId value="MAGETWO-99691"/> <group value="sales"/> <group value="catalogRule"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> <!--Create the catalog price rule --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index e487c62b96727..255a7a91f9b10 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -29,7 +29,7 @@ <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 0" /> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create order via Admin--> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index ed536bd3351f9..01021ad745f70 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -26,7 +26,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create order via Admin--> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index 1490fc1a1a388..9268e9e728658 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -25,7 +25,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create order via Admin--> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 4c1f16192c88c..9da5afffb48e5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -87,6 +87,8 @@ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigurableProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCustomerGridFilter"/> <actionGroup ref="logout" stepKey="logout"/> </after> @@ -98,6 +100,7 @@ <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickOnCreateOrderButton"/> <waitForPageLoad stepKey="waitForOrderPageToLoad"/> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(_defaultStore.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(_defaultStore.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <!--Add configurable product to order--> <actionGroup ref="addConfigurableProductToOrderFromAdmin" stepKey="addConfigurableProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index 93f4233af90e4..ec0f97e418c8c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -33,7 +33,7 @@ <argument name="ruleName" value="{{ApiSalesRule.name}}"/> </actionGroup> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct" stepKey="deleteCategory1"/> </after> @@ -58,7 +58,7 @@ <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <!-- Place an order from Storefront as a Guest --> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml index 9e794ce079b6e..0a39e8a0ac852 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml @@ -54,6 +54,9 @@ <requiredEntity createDataKey="createBundleOption"/> <requiredEntity createDataKey="createSecondProduct"/> </createData> + <!-- Change configuration --> + <magentoCLI command="config:set reports/options/enabled 1" stepKey="enableReportModule"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> </before> <after> @@ -73,6 +76,9 @@ <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Change configuration --> + <magentoCLI command="config:set reports/options/enabled 0" stepKey="disableReportModule"/> </after> <!-- Login as customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml index 35dc49d2b8a43..aed7447050e5f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml @@ -57,6 +57,8 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct"/> </createData> + <!-- Change configuration --> + <magentoCLI command="config:set reports/options/enabled 1" stepKey="enableReportModule"/> </before> <after> <!-- Admin logout --> @@ -75,6 +77,9 @@ <!-- Delete category --> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Change configuration --> + <magentoCLI command="config:set reports/options/enabled 0" stepKey="disableReportModule"/> </after> <!-- Login as customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml index b8772f24a2a42..0e58bb84988a2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml @@ -129,7 +129,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <scrollTo selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="scrollToLimiter"/> - <selectOption userInput="30" selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="selectLimitOnPage"/> + <selectOption userInput="36" selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="selectLimitOnPage"/> <waitForPageLoad stepKey="waitForLoadProducts"/> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml index 9909fca44fe2c..3ff8a7791d88b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml @@ -125,7 +125,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <scrollTo selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="scrollToLimiter"/> - <selectOption userInput="30" selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="selectLimitOnPage"/> + <selectOption userInput="36" selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="selectLimitOnPage"/> <waitForPageLoad stepKey="waitForLoadProducts"/> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml index 5c0d405102464..0bd8ab4855e97 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml @@ -19,6 +19,7 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI command="downloadable:domains:add" arguments="example.com static.magento.com" stepKey="addDownloadableDomain"/> <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -207,6 +208,7 @@ <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRate"/> </before> <after> + <magentoCLI command="downloadable:domains:remove" arguments="example.com static.magento.com" stepKey="removeDownloadableDomain"/> <deleteData createDataKey="downloadableProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> @@ -275,4 +277,4 @@ <see userInput="Flat Rate - Fixed" selector="{{StorefrontOrderDetailsSection.shippingMethod}}" stepKey="assertShippingMethodOnPrintOrder"/> <switchToPreviousTab stepKey="switchToPreviousTab"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php index 94148cc515382..2b08daf02134e 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php @@ -95,6 +95,11 @@ protected function setUp() '_orderCreate' => $this->orderCreate ] ); + + // Do not display VAT validation button on edit order address form + // Emulate fix done in controller + /** @see \Magento\Sales\Controller\Adminhtml\Order\Address::execute */ + $this->addressBlock->setDisplayVatValidationButton(false); } public function testGetForm() diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php index 053df53949296..9fe3042fa6bdf 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -186,7 +189,8 @@ protected function setUp() 'invoiceCommentSender' => $this->commentSenderMock, 'resultPageFactory' => $this->resultPageFactoryMock, 'resultRawFactory' => $this->resultRawFactoryMock, - 'resultJsonFactory' => $this->resultJsonFactoryMock + 'resultJsonFactory' => $this->resultJsonFactoryMock, + 'invoiceRepository' => $this->invoiceRepository ] ); @@ -230,8 +234,9 @@ public function testExecute() $invoiceMock->expects($this->once()) ->method('addComment') ->with($data['comment'], false, false); - $invoiceMock->expects($this->once()) - ->method('save'); + $this->invoiceRepository->expects($this->once()) + ->method('save') + ->with($invoiceMock); $this->invoiceRepository->expects($this->once()) ->method('get') @@ -307,11 +312,11 @@ public function testExecuteModelException() public function testExecuteException() { $response = ['error' => true, 'message' => 'Cannot add new comment.']; - $e = new \Exception('test'); + $error = new \Exception('test'); $this->requestMock->expects($this->once()) ->method('getParam') - ->will($this->throwException($e)); + ->will($this->throwException($error)); $this->resultJsonFactoryMock->expects($this->once()) ->method('create') diff --git a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php deleted file mode 100644 index e424cae85f223..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php +++ /dev/null @@ -1,84 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sales\Test\Unit\Cron; - -use \Magento\Sales\Cron\CleanExpiredQuotes; - -/** - * Tests Magento\Sales\Cron\CleanExpiredQuotes - */ -class CleanExpiredQuotesTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Store\Model\StoresConfig|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storesConfigMock; - - /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteFactoryMock; - - /** - * @var \Magento\Sales\Cron\CleanExpiredQuotes - */ - protected $observer; - - protected function setUp() - { - $this->storesConfigMock = $this->createMock(\Magento\Store\Model\StoresConfig::class); - - $this->quoteFactoryMock = $this->getMockBuilder( - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->observer = new CleanExpiredQuotes($this->storesConfigMock, $this->quoteFactoryMock); - } - - /** - * @param array $lifetimes - * @param array $additionalFilterFields - * @dataProvider cleanExpiredQuotesDataProvider - */ - public function testExecute($lifetimes, $additionalFilterFields) - { - $this->storesConfigMock->expects($this->once()) - ->method('getStoresConfigByPath') - ->with($this->equalTo('checkout/cart/delete_quote_after')) - ->will($this->returnValue($lifetimes)); - - $quotesMock = $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->quoteFactoryMock->expects($this->exactly(count($lifetimes))) - ->method('create') - ->will($this->returnValue($quotesMock)); - $quotesMock->expects($this->exactly((3 + count($additionalFilterFields)) * count($lifetimes))) - ->method('addFieldToFilter'); - if (!empty($lifetimes)) { - $quotesMock->expects($this->exactly(count($lifetimes))) - ->method('walk') - ->with('delete'); - } - $this->observer->setExpireQuotesAdditionalFilterFields($additionalFilterFields); - $this->observer->execute(); - } - - /** - * @return array - */ - public function cleanExpiredQuotesDataProvider() - { - return [ - [[], []], - [[1 => 100, 2 => 200], []], - [[1 => 100, 2 => 200], ['field1' => 'condition1', 'field2' => 'condition2']], - ]; - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php index 565d51ff515a2..f32ce7aa4715b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php @@ -100,16 +100,18 @@ public function testCollect($orderData, $creditmemoData, $expectedResults) } $this->creditmemo->expects($this->any()) ->method('roundPrice') - ->will($this->returnCallback( - function ($price, $type) use (&$roundingDelta) { - if (!isset($roundingDelta[$type])) { - $roundingDelta[$type] = 0; + ->will( + $this->returnCallback( + function ($price, $type) use (&$roundingDelta) { + if (!isset($roundingDelta[$type])) { + $roundingDelta[$type] = 0; + } + $roundedPrice = round($price + $roundingDelta[$type], 2); + $roundingDelta[$type] = $price - $roundedPrice; + return $roundedPrice; } - $roundedPrice = round($price + $roundingDelta[$type], 2); - $roundingDelta[$type] = $price - $roundedPrice; - return $roundedPrice; - } - )); + ) + ); $this->model->collect($this->creditmemo); @@ -610,6 +612,143 @@ public function collectDataProvider() ], ]; + // scenario 6: 2 items, 2 invoiced, price includes tax, full discount, free shipping + // partial credit memo, make sure that discount tax compensation (with 100 % discount) is calculated correctly + $result['collect_with_full_discount_product_price'] = [ + 'order_data' => [ + 'data_fields' => [ + 'discount_amount' => -200.00, + 'discount_invoiced' => -200.00, + 'subtotal' => 181.82, + 'subtotal_incl_tax' => 200, + 'base_subtotal' => 181.82, + 'base_subtotal_incl_tax' => 200, + 'subtotal_invoiced' => 181.82, + 'discount_tax_compensation_amount' => 18.18, + 'discount_tax_compensation_invoiced' => 18.18, + 'base_discount_tax_compensation_amount' => 18.18, + 'base_discount_tax_compensation_invoiced' => 18.18, + 'grand_total' => 0, + 'base_grand_total' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + 'tax_amount' => 0, + 'base_tax_amount' => 0, + 'tax_invoiced' => 0, + 'base_tax_invoiced' => 0, + 'tax_refunded' => 0, + 'base_tax_refunded' => 0, + 'base_shipping_amount' => 0, + ], + ], + 'creditmemo_data' => [ + 'items' => [ + 'item_1' => [ + 'order_item' => [ + 'qty_invoiced' => 1, + 'tax_amount' => 0, + 'tax_invoiced' => 0, + 'tax_refunded' => null, + 'base_tax_amount' => 0, + 'base_tax_invoiced' => 0, + 'base_tax_refunded' => 0, + 'tax_percent' => 10, + 'qty_refunded' => 0, + 'discount_percent' => 100, + 'discount_amount' => 100, + 'base_discount_amount' => 100, + 'discount_invoiced' => 100, + 'base_discount_invoiced' => 100, + 'row_total' => 90.91, + 'base_row_total' => 90.91, + 'row_invoiced' => 90.91, + 'base_row_invoiced' => 90.91, + 'price_incl_tax' => 100, + 'base_price_incl_tax' => 100, + 'row_total_incl_tax' => 100, + 'base_row_total_incl_tax' => 100, + 'discount_tax_compensation_amount' => 9.09, + 'base_discount_tax_compensation_amount' => 9.09, + 'discount_tax_compensation_invoiced' => 9.09, + 'base_discount_tax_compensation_invoiced' => 9.09, + ], + 'is_last' => true, + 'qty' => 1, + ], + 'item_2' => [ + 'order_item' => [ + 'qty_invoiced' => 1, + 'tax_amount' => 0, + 'tax_invoiced' => 0, + 'tax_refunded' => null, + 'base_tax_amount' => 0, + 'base_tax_invoiced' => 0, + 'base_tax_refunded' => null, + 'tax_percent' => 10, + 'qty_refunded' => 0, + 'discount_percent' => 100, + 'discount_amount' => 100, + 'base_discount_amount' => 100, + 'discount_invoiced' => 100, + 'base_discount_invoiced' => 100, + 'row_total' => 90.91, + 'base_row_total' => 90.91, + 'row_invoiced' => 90.91, + 'base_row_invoiced' => 90.91, + 'price_incl_tax' => 100, + 'base_price_incl_tax' => 100, + 'row_total_incl_tax' => 100, + 'base_row_total_incl_tax' => 100, + 'discount_tax_compensation_amount' => 9.09, + 'base_discount_tax_compensation_amount' => 9.09, + 'discount_tax_compensation_invoiced' => 9.09, + 'base_discount_tax_compensation_invoiced' => 9.09, + ], + 'is_last' => false, + 'qty' => 0, + ], + ], + 'is_last' => false, + 'data_fields' => [ + 'grand_total' => -9.09, + 'base_grand_total' => -9.09, + 'base_shipping_amount' => 0, + 'tax_amount' => 0, + 'base_tax_amount' => 0, + 'invoice' => new MagentoObject( + [ + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + ] + ), + ], + ], + 'expected_results' => [ + 'creditmemo_items' => [ + 'item_1' => [ + 'tax_amount' => 0, + 'base_tax_amount' => 0, + ], + 'item_2' => [ + 'tax_amount' => 0, + 'base_tax_amount' => 0, + ], + ], + 'creditmemo_data' => [ + 'grand_total' => 0, + 'base_grand_total' => 0, + 'tax_amount' => 0, + 'base_tax_amount' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + ], + ], + ]; + return $result; } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php index 68e1c7c17cd1c..d255f88dea359 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(CreditmemoCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php index 3da6dfe78eb40..1e3ff11ea73c1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(CreditmemoIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php index b7ec911212254..5eeaa12a736f6 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(InvoiceCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php index 4a63541bc05f2..3328d2f35b2b3 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(InvoiceIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php index 1dc53b97711ab..0892ba34114be 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(OrderCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php index f554c2aeef168..54d1ab872fb1d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(OrderIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php index a8d676be2a2f1..ff62b46e0cac9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(ShipmentCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php index 503646a5eac61..bccf109783913 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(ShipmentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php index 2297d6aa711cf..2f4e0e927db2c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php @@ -92,12 +92,16 @@ public function stepMockSetup() $this->storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getStoreId', '__wakeup']); - $this->orderMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, [ + $this->orderMock = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + [ 'getStore', 'getBillingAddress', 'getPayment', '__wakeup', 'getCustomerIsGuest', 'getCustomerName', 'getCustomerEmail', 'getShippingAddress', 'setSendEmail', - 'setEmailSent' - ]); + 'setEmailSent', 'getCreatedAtFormatted', 'getIsNotVirtual', + 'getEmailCustomerNote', 'getFrontendStatusLabel' + ] + ); $this->orderMock->expects($this->any()) ->method('getStore') ->will($this->returnValue($this->storeMock)); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php index 3b97c8451d32c..40e7ce4568d20 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php @@ -59,6 +59,16 @@ public function testSendVirtualOrder() { $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, true); $billingAddress = $this->addressMock; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -70,7 +80,11 @@ public function testSendVirtualOrder() 'billing' => $billingAddress, 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -83,6 +97,15 @@ public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -102,7 +125,11 @@ public function testSendTrueWithCustomerCopy() 'billing' => $billingAddress, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -115,6 +142,15 @@ public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -134,7 +170,11 @@ public function testSendTrueWithoutCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php index 287daa2fba4b9..72a51a15db592 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -90,6 +90,9 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $comment = 'comment_test'; $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Processing'; + $isNotVirtual = true; $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') @@ -118,6 +121,22 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->method('getCustomerNote') ->willReturn($comment); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -129,7 +148,13 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); @@ -211,9 +236,28 @@ public function sendDataProvider() public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expectedShippingAddress) { $billingAddress = 'address_test'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') ->with(false); @@ -247,7 +291,14 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $billingAddress + 'formattedBillingAddress' => $billingAddress, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] + ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php index 3e29bf04e358d..f0a05586cd972 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php @@ -63,9 +63,20 @@ public function testSendTrueWithCustomerCopy() $billingAddress = $this->addressMock; $this->stepAddressFormat($billingAddress); $comment = 'comment_test'; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Processing'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') ->will($this->returnValue(false)); + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); @@ -80,7 +91,11 @@ public function testSendTrueWithCustomerCopy() 'billing' => $billingAddress, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -93,12 +108,22 @@ public function testSendTrueWithCustomerCopy() public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Processing'; $this->stepAddressFormat($billingAddress); $comment = 'comment_test'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); @@ -113,7 +138,11 @@ public function testSendTrueWithoutCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -127,6 +156,16 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->identityContainerMock->expects($this->once()) ->method('isEnabled') @@ -142,7 +181,11 @@ public function testSendVirtualOrder() 'comment' => '', 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php index 3315ec8eb4196..00a1855055a84 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -90,6 +90,9 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $comment = 'comment_test'; $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $customerName = 'Test Customer'; + $isNotVirtual = true; + $frontendStatusLabel = 'Processing'; $this->invoiceMock->expects($this->once()) ->method('setSendEmail') @@ -116,6 +119,22 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->method('getShippingAddress') ->willReturn($addressMock); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->invoiceMock->expects($this->once()) ->method('getCustomerNoteNotify') ->willReturn($customerNoteNotify); @@ -135,7 +154,13 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); @@ -216,6 +241,9 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte { $billingAddress = 'address_test'; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->invoiceMock->expects($this->once()) ->method('setSendEmail') @@ -238,6 +266,21 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->invoiceMock->expects($this->once()) ->method('getCustomerNoteNotify') ->willReturn(false); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') @@ -250,7 +293,13 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $billingAddress + 'formattedBillingAddress' => $billingAddress, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => false, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php index e5d6cacb25637..049cc75d3e42c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php @@ -40,10 +40,18 @@ public function testSendTrue() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName='Test Customer'; + $frontendStatusLabel='Processing'; $this->stepAddressFormat($billingAddress); $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->identityContainerMock->expects($this->once()) ->method('isEnabled') @@ -58,7 +66,11 @@ public function testSendTrue() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -72,10 +84,18 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); + $customerName='Test Customer'; + $frontendStatusLabel='Complete'; $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -86,7 +106,11 @@ public function testSendVirtualOrder() 'billing' => $this->addressMock, 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index bfea2d63ef1bb..a033e41dd8e8b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -56,11 +56,16 @@ protected function setUp() * @param $senderSendException * @return void * @dataProvider sendDataProvider + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $senderSendException) { $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $createdAtFormatted='Oct 14, 2019, 4:11:58 PM'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Processing'; + $isNotVirtual = true; $this->orderMock->expects($this->once()) ->method('setSendEmail') @@ -96,6 +101,27 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen ->method('getShippingAddress') ->willReturn($addressMock); + $this->orderMock->expects($this->once()) + ->method('getCreatedAtFormatted') + ->with(2) + ->willReturn($createdAtFormatted); + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -105,7 +131,15 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'created_at_formatted'=>$createdAtFormatted, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] + ] ); @@ -204,6 +238,10 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte { $address = 'address_test'; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $createdAtFormatted='Oct 14, 2019, 4:11:58 PM'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->orderMock->expects($this->once()) ->method('setSendEmail') @@ -231,6 +269,27 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->stepAddressFormat($addressMock, $isVirtualOrder); + $this->orderMock->expects($this->once()) + ->method('getCreatedAtFormatted') + ->with(2) + ->willReturn($createdAtFormatted); + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -240,7 +299,14 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'created_at_formatted'=>$createdAtFormatted, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php index f5a2e4d0148cd..90664216e87bc 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php @@ -56,6 +56,8 @@ public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName='Test Customer'; + $frontendStatusLabel='Processing'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -65,6 +67,12 @@ public function testSendTrueWithCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -76,7 +84,11 @@ public function testSendTrueWithCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -89,6 +101,8 @@ public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName='Test Customer'; + $frontendStatusLabel='Processing'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -98,6 +112,12 @@ public function testSendTrueWithoutCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -109,7 +129,11 @@ public function testSendTrueWithoutCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -123,10 +147,18 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); + $customerName='Test Customer'; + $frontendStatusLabel='Complete'; $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -138,7 +170,12 @@ public function testSendVirtualOrder() 'comment' => '', 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] + ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php index 96bbb1aea7abd..dc6fc53e5ec43 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -90,6 +90,9 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $comment = 'comment_test'; $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $customerName = 'Test Customer'; + $isNotVirtual = true; + $frontendStatusLabel = 'Processing'; $this->shipmentMock->expects($this->once()) ->method('setSendEmail') @@ -124,6 +127,22 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->method('getCustomerNote') ->willReturn($comment); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -135,7 +154,13 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); @@ -216,6 +241,9 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte { $address = 'address_test'; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->shipmentMock->expects($this->once()) ->method('setSendEmail') @@ -239,6 +267,22 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->method('getCustomerNoteNotify') ->willReturn(false); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -250,7 +294,13 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => false, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index adfb697e70331..756048d287e46 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php @@ -37,11 +37,6 @@ class SenderBuilderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $templateId = 'test_template_id'; - $templateOptions = ['option1', 'option2']; - $templateVars = ['var1', 'var2']; - $emailIdentity = 'email_identity_test'; - $emailCopyTo = ['example@mail.com']; $this->templateContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\Template::class, @@ -83,36 +78,6 @@ protected function setUp() ] ); - $this->templateContainerMock->expects($this->once()) - ->method('getTemplateId') - ->will($this->returnValue($templateId)); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($this->equalTo($templateId)); - $this->templateContainerMock->expects($this->once()) - ->method('getTemplateOptions') - ->will($this->returnValue($templateOptions)); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with($this->equalTo($templateOptions)); - $this->templateContainerMock->expects($this->once()) - ->method('getTemplateVars') - ->will($this->returnValue($templateVars)); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with($this->equalTo($templateVars)); - - $this->identityContainerMock->expects($this->once()) - ->method('getEmailIdentity') - ->will($this->returnValue($emailIdentity)); - $this->transportBuilder->expects($this->once()) - ->method('setFromByScope') - ->with($this->equalTo($emailIdentity), 1); - - $this->identityContainerMock->expects($this->once()) - ->method('getEmailCopyTo') - ->will($this->returnValue($emailCopyTo)); - $this->senderBuilder = new SenderBuilder( $this->templateContainerMock, $this->identityContainerMock, @@ -122,6 +87,7 @@ protected function setUp() public function testSend() { + $this->setExpectedCount(1); $customerName = 'test_name'; $customerEmail = 'test_email'; $identity = 'email_identity_test'; @@ -142,20 +108,20 @@ public function testSend() $this->identityContainerMock->expects($this->once()) ->method('getCustomerName') ->will($this->returnValue($customerName)); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(1)) ->method('getStore') ->willReturn($this->storeMock); $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(1)) ->method('setFromByScope') ->with($identity, 1); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(1)) ->method('addTo') ->with($this->equalTo($customerEmail), $this->equalTo($customerName)); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(1)) ->method('getTransport') ->will($this->returnValue($transportMock)); @@ -164,6 +130,7 @@ public function testSend() public function testSendCopyTo() { + $this->setExpectedCount(2); $identity = 'email_identity_test'; $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class @@ -172,22 +139,66 @@ public function testSendCopyTo() ->method('getCustomerEmail'); $this->identityContainerMock->expects($this->never()) ->method('getCustomerName'); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($this->equalTo('example@mail.com')); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(2)) + ->method('addTo'); + $this->transportBuilder->expects($this->exactly(2)) ->method('setFromByScope') ->with($identity, 1); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('getStore') ->willReturn($this->storeMock); - $this->storeMock->expects($this->once()) + $this->storeMock->expects($this->exactly(2)) ->method('getId') ->willReturn(1); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(2)) ->method('getTransport') ->will($this->returnValue($transportMock)); $this->senderBuilder->sendCopyTo(); } + + /** + * Sets expected count invocation. + * + * @param int $count + */ + private function setExpectedCount(int $count = 1) + { + + $templateId = 'test_template_id'; + $templateOptions = ['option1', 'option2']; + $templateVars = ['var1', 'var2']; + $emailIdentity = 'email_identity_test'; + $emailCopyTo = ['example@mail.com', 'example2@mail.com']; + + $this->templateContainerMock->expects($this->exactly($count)) + ->method('getTemplateId') + ->will($this->returnValue($templateId)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setTemplateIdentifier') + ->with($this->equalTo($templateId)); + $this->templateContainerMock->expects($this->exactly($count)) + ->method('getTemplateOptions') + ->will($this->returnValue($templateOptions)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setTemplateOptions') + ->with($this->equalTo($templateOptions)); + $this->templateContainerMock->expects($this->exactly($count)) + ->method('getTemplateVars') + ->will($this->returnValue($templateVars)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setTemplateVars') + ->with($this->equalTo($templateVars)); + + $this->identityContainerMock->expects($this->exactly($count)) + ->method('getEmailIdentity') + ->will($this->returnValue($emailIdentity)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setFromByScope') + ->with($this->equalTo($emailIdentity), 1); + + $this->identityContainerMock->expects($this->once()) + ->method('getEmailCopyTo') + ->will($this->returnValue($emailCopyTo)); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index 705d2face2308..bd6487caff7dd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model; use Magento\Catalog\Api\Data\ProductInterface; @@ -17,6 +18,8 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchCriteria; use Magento\Sales\Api\Data\OrderItemSearchResultInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use PHPUnit\Framework\MockObject\MockObject; /** * Test class for \Magento\Sales\Model\Order @@ -24,6 +27,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.TooManyFields) */ class OrderTest extends \PHPUnit\Framework\TestCase { @@ -102,6 +106,11 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ private $searchCriteriaBuilder; + /** + * @var MockObject|ScopeConfigInterface $scopeConfigMock + */ + private $scopeConfigMock; + protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -125,14 +134,17 @@ protected function setUp() \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class, ['create'] ); - $this->item = $this->createPartialMock(\Magento\Sales\Model\ResourceModel\Order\Item::class, [ + $this->item = $this->createPartialMock( + \Magento\Sales\Model\ResourceModel\Order\Item::class, + [ 'isDeleted', 'getQtyToInvoice', 'getParentItemId', 'getQuoteItemId', 'getLockedDoInvoice', 'getProductId', - ]); + ] + ); $this->salesOrderCollectionMock = $this->getMockBuilder( \Magento\Sales\Model\ResourceModel\Order\Collection::class )->disableOriginalConstructor() @@ -168,6 +180,7 @@ protected function setUp() ->setMethods(['addFilter', 'create']) ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $this->order = $helper->getObject( \Magento\Sales\Model\Order::class, [ @@ -182,7 +195,8 @@ protected function setUp() 'localeResolver' => $this->localeResolver, 'timezone' => $this->timezone, 'itemRepository' => $this->itemRepository, - 'searchCriteriaBuilder' => $this->searchCriteriaBuilder + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'scopeConfig' => $this->scopeConfigMock ] ); } @@ -354,6 +368,51 @@ public function testCanInvoice() $this->assertTrue($this->order->canInvoice()); } + /** + * Ensure customer name returned correctly. + * + * @dataProvider customerNameProvider + * @param array $expectedData + */ + public function testGetCustomerName(array $expectedData) + { + $this->order->setCustomerFirstname($expectedData['first_name']); + $this->order->setCustomerSuffix($expectedData['customer_suffix']); + $this->order->setCustomerPrefix($expectedData['customer_prefix']); + $this->scopeConfigMock->expects($this->exactly($expectedData['invocation'])) + ->method('isSetFlag') + ->willReturn(true); + $this->assertEquals($expectedData['expected_name'], $this->order->getCustomerName()); + } + + /** + * Customer name data provider + */ + public function customerNameProvider() + { + return + [ + [ + [ + 'first_name' => null, + 'invocation' => 0, + 'expected_name' => 'Guest', + 'customer_suffix' => 'smith', + 'customer_prefix' => 'mr.' + ] + ], + [ + [ + 'first_name' => 'Smith', + 'invocation' => 1, + 'expected_name' => 'mr. Smith Carl', + 'customer_suffix' => 'Carl', + 'customer_prefix' => 'mr.' + ] + ] + ]; + } + /** * @param string $status * @@ -819,9 +878,10 @@ public function testCanVoidPayment($actionFlags, $orderState) if ($orderState == \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW) { $canVoidOrder = false; } - if ($orderState == \Magento\Sales\Model\Order::STATE_HOLDED && (!isset( - $actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD] - ) || $actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD] !== false) + if ($orderState == \Magento\Sales\Model\Order::STATE_HOLDED && + (!isset($actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD]) || + $actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD] !== false + ) ) { $canVoidOrder = false; } @@ -1193,6 +1253,9 @@ public function testGetCreatedAtFormattedUsesCorrectLocale() $this->order->getCreatedAtFormatted(\IntlDateFormatter::SHORT); } + /** + * @return array + */ public function notInvoicingStatesProvider() { return [ @@ -1202,6 +1265,9 @@ public function notInvoicingStatesProvider() ]; } + /** + * @return array + */ public function canNotCreditMemoStatesProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index 99a411c43c247..30513571fb71d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -103,6 +103,9 @@ public function testCheck( $this->assertEquals($expectedState, $this->orderMock->getState()); } + /** + * @return array + */ public function stateCheckDataProvider() { return [ diff --git a/app/code/Magento/Sales/ViewModel/CreditMemo/Create/UpdateTotalsButton.php b/app/code/Magento/Sales/ViewModel/CreditMemo/Create/UpdateTotalsButton.php new file mode 100644 index 0000000000000..707f5ef363f66 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/CreditMemo/Create/UpdateTotalsButton.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Sales\ViewModel\CreditMemo\Create; + +use Magento\Backend\Block\Widget\Button; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items; + +/** + * View model to add Update Totals button for new Credit Memo + */ +class UpdateTotalsButton implements \Magento\Framework\View\Element\Block\ArgumentInterface +{ + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var Items + */ + private $items; + + /** + * @param LayoutInterface $layout + * @param Items $items + */ + public function __construct( + LayoutInterface $layout, + Items $items + ) { + $this->layout = $layout; + $this->items = $items; + } + + /** + * Get Update Totals block html. + * + * @return string + */ + public function getUpdateTotalsButton(): string + { + $block = $this->createUpdateTotalsBlock(); + + return $block->toHtml(); + } + + /** + * Create Update Totals block. + * + * @return BlockInterface + */ + private function createUpdateTotalsBlock(): BlockInterface + { + $onclick = "submitAndReloadArea($('creditmemo_item_container'),'" . $this->items->getUpdateUrl() . "')"; + $block = $this->layout->addBlock(Button::class, 'update_totals_button', 'order_items'); + $block->setData( + [ + 'label' => __('Update Totals'), + 'class' => 'update-totals-button secondary', + 'onclick' => $onclick, + ] + ); + + return $block; + } +} diff --git a/app/code/Magento/Sales/etc/adminhtml/system.xml b/app/code/Magento/Sales/etc/adminhtml/system.xml index d1e5680b883c2..473d3068d69c7 100644 --- a/app/code/Magento/Sales/etc/adminhtml/system.xml +++ b/app/code/Magento/Sales/etc/adminhtml/system.xml @@ -93,12 +93,12 @@ <field id="amount" translate="label comment" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Amount</label> <validate>validate-number validate-greater-than-zero</validate> - <comment>Subtotal after discount</comment> + <comment>Subtotal after discount.</comment> </field> <field id="include_discount_amount" translate="label" sortOrder="12" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Include Discount Amount</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment>Choosing yes will be used subtotal after discount, otherwise only subtotal will be used</comment> + <comment>Choosing yes will be used subtotal after discount, otherwise only subtotal will be used.</comment> </field> <field id="tax_including" translate="label" sortOrder="15" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Include Tax to Amount</label> @@ -182,7 +182,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Order Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -212,7 +212,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Order Comment Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -242,7 +242,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Invoice Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -272,7 +272,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Invoice Comment Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -302,7 +302,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Shipment Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -332,7 +332,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Shipment Comment Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -362,7 +362,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Credit Memo Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -392,7 +392,7 @@ </field> <field id="copy_to" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Send Credit Memo Comment Email Copy To</label> - <comment>Comma-separated</comment> + <comment>Comma-separated.</comment> <validate>validate-emails</validate> </field> <field id="copy_method" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 1f781604491bf..ea7c768b0a786 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="sales_order" resource="sales" engine="innodb" comment="Sales Flat Order"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="state" nullable="true" length="32" comment="State"/> <column xsi:type="varchar" name="status" nullable="true" length="32" comment="Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="255" comment="Coupon Code"/> @@ -19,9 +19,9 @@ <column xsi:type="smallint" name="is_virtual" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Virtual"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> <column xsi:type="decimal" name="base_discount_canceled" scale="4" precision="20" unsigned="false" @@ -145,7 +145,7 @@ <column xsi:type="smallint" name="customer_note_notify" padding="5" unsigned="true" nullable="true" identity="false" comment="Customer Note Notify"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="edit_increment" padding="11" unsigned="false" nullable="true" identity="false" comment="Edit Increment"/> @@ -158,11 +158,11 @@ <column xsi:type="int" name="payment_auth_expiration" padding="11" unsigned="false" nullable="true" identity="false" comment="Payment Authorization Expiration"/> <column xsi:type="int" name="quote_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Address Id"/> + comment="Quote Address ID"/> <column xsi:type="int" name="quote_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Id"/> + comment="Quote ID"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" @@ -188,7 +188,7 @@ <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> <column xsi:type="datetime" name="customer_dob" on_update="false" nullable="true" comment="Customer Dob"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="32" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="32" comment="Increment ID"/> <column xsi:type="varchar" name="applied_rule_ids" nullable="true" length="128" comment="Applied Rule Ids"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="128" comment="Customer Email"/> @@ -201,21 +201,21 @@ <column xsi:type="varchar" name="customer_taxvat" nullable="true" length="32" comment="Customer Taxvat"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> - <column xsi:type="varchar" name="ext_customer_id" nullable="true" length="32" comment="Ext Customer Id"/> - <column xsi:type="varchar" name="ext_order_id" nullable="true" length="32" comment="Ext Order Id"/> + <column xsi:type="varchar" name="ext_customer_id" nullable="true" length="32" comment="Ext Customer ID"/> + <column xsi:type="varchar" name="ext_order_id" nullable="true" length="32" comment="Ext Order ID"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="3" comment="Global Currency Code"/> <column xsi:type="varchar" name="hold_before_state" nullable="true" length="32" comment="Hold Before State"/> <column xsi:type="varchar" name="hold_before_status" nullable="true" length="32" comment="Hold Before Status"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> <column xsi:type="varchar" name="original_increment_id" nullable="true" length="32" - comment="Original Increment Id"/> - <column xsi:type="varchar" name="relation_child_id" nullable="true" length="32" comment="Relation Child Id"/> + comment="Original Increment ID"/> + <column xsi:type="varchar" name="relation_child_id" nullable="true" length="32" comment="Relation Child ID"/> <column xsi:type="varchar" name="relation_child_real_id" nullable="true" length="32" - comment="Relation Child Real Id"/> - <column xsi:type="varchar" name="relation_parent_id" nullable="true" length="32" comment="Relation Parent Id"/> + comment="Relation Child Real ID"/> + <column xsi:type="varchar" name="relation_parent_id" nullable="true" length="32" comment="Relation Parent ID"/> <column xsi:type="varchar" name="relation_parent_real_id" nullable="true" length="32" - comment="Relation Parent Real Id"/> + comment="Relation Parent Real ID"/> <column xsi:type="varchar" name="remote_ip" nullable="true" length="45" comment="Remote Ip"/> <column xsi:type="varchar" name="shipping_method" nullable="true" length="120"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> @@ -297,13 +297,13 @@ </table> <table name="sales_order_grid" resource="sales" engine="innodb" comment="Sales Flat Order Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="status" nullable="true" length="32" comment="Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <column xsi:type="decimal" name="base_total_paid" scale="4" precision="20" unsigned="false" nullable="true" @@ -312,7 +312,7 @@ comment="Grand Total"/> <column xsi:type="decimal" name="total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Paid"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="255" comment="Order Currency Code"/> @@ -386,17 +386,17 @@ </table> <table name="sales_order_address" resource="sales" engine="innodb" comment="Sales Flat Order Address"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="customer_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Address Id"/> + comment="Customer Address ID"/> <column xsi:type="int" name="quote_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Address Id"/> + comment="Quote Address ID"/> <column xsi:type="int" name="region_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Region Id"/> + comment="Region ID"/> <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="varchar" name="fax" nullable="true" length="255" comment="Fax"/> <column xsi:type="varchar" name="region" nullable="true" length="255" comment="Region"/> <column xsi:type="varchar" name="postcode" nullable="true" length="255" comment="Postcode"/> @@ -405,17 +405,17 @@ <column xsi:type="varchar" name="city" nullable="true" length="255" comment="City"/> <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> <column xsi:type="varchar" name="telephone" nullable="true" length="255" comment="Phone Number"/> - <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country Id"/> + <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country ID"/> <column xsi:type="varchar" name="firstname" nullable="true" length="255" comment="Firstname"/> <column xsi:type="varchar" name="address_type" nullable="true" length="255" comment="Address Type"/> <column xsi:type="varchar" name="prefix" nullable="true" length="255" comment="Prefix"/> <column xsi:type="varchar" name="middlename" nullable="true" length="255" comment="Middlename"/> <column xsi:type="varchar" name="suffix" nullable="true" length="255" comment="Suffix"/> <column xsi:type="varchar" name="company" nullable="true" length="255" comment="Company"/> - <column xsi:type="text" name="vat_id" nullable="true" comment="Vat Id"/> + <column xsi:type="text" name="vat_id" nullable="true" comment="Vat ID"/> <column xsi:type="smallint" name="vat_is_valid" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Is Valid"/> - <column xsi:type="text" name="vat_request_id" nullable="true" comment="Vat Request Id"/> + <column xsi:type="text" name="vat_request_id" nullable="true" comment="Vat Request ID"/> <column xsi:type="text" name="vat_request_date" nullable="true" comment="Vat Request Date"/> <column xsi:type="smallint" name="vat_request_success" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Request Success"/> @@ -431,9 +431,9 @@ </table> <table name="sales_order_status_history" resource="sales" engine="innodb" comment="Sales Flat Order Status History"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="is_customer_notified" padding="11" unsigned="false" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -459,21 +459,21 @@ </table> <table name="sales_order_item" resource="sales" engine="innodb" comment="Sales Flat Order Item"> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Order Id"/> + default="0" comment="Order ID"/> <column xsi:type="int" name="parent_item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Item Id"/> + comment="Parent Item ID"/> <column xsi:type="int" name="quote_item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Quote Item Id"/> + comment="Quote Item ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_type" nullable="true" length="255" comment="Product Type"/> <column xsi:type="text" name="product_options" nullable="true" comment="Product Options"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" @@ -549,7 +549,7 @@ nullable="true" comment="Base Tax Before Discount"/> <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> - <column xsi:type="varchar" name="ext_order_item_id" nullable="true" length="255" comment="Ext Order Item Id"/> + <column xsi:type="varchar" name="ext_order_item_id" nullable="true" length="255" comment="Ext Order Item ID"/> <column xsi:type="smallint" name="locked_do_invoice" padding="5" unsigned="true" nullable="true" identity="false" comment="Locked Do Invoice"/> <column xsi:type="smallint" name="locked_do_ship" padding="5" unsigned="true" nullable="true" identity="false" @@ -602,9 +602,9 @@ </table> <table name="sales_order_payment" resource="sales" engine="innodb" comment="Sales Flat Order Payment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="base_shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Captured"/> <column xsi:type="decimal" name="shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" @@ -642,7 +642,7 @@ <column xsi:type="decimal" name="base_amount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Canceled"/> <column xsi:type="int" name="quote_payment_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Payment Id"/> + comment="Quote Payment ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="varchar" name="cc_exp_month" nullable="true" length="12" comment="Cc Exp Month"/> <column xsi:type="varchar" name="cc_ss_start_year" nullable="true" length="12" comment="Cc Ss Start Year"/> @@ -663,7 +663,7 @@ <column xsi:type="varchar" name="cc_ss_start_month" nullable="true" length="128" comment="Cc Ss Start Month"/> <column xsi:type="varchar" name="echeck_account_type" nullable="true" length="255" comment="Echeck Account Type"/> - <column xsi:type="varchar" name="last_trans_id" nullable="true" length="255" comment="Last Trans Id"/> + <column xsi:type="varchar" name="last_trans_id" nullable="true" length="255" comment="Last Trans ID"/> <column xsi:type="varchar" name="cc_cid_status" nullable="true" length="32" comment="Cc Cid Status"/> <column xsi:type="varchar" name="cc_owner" nullable="true" length="128" comment="Cc Owner"/> <column xsi:type="varchar" name="cc_type" nullable="true" length="32" comment="Cc Type"/> @@ -681,7 +681,7 @@ comment="Echeck Account Name"/> <column xsi:type="varchar" name="cc_avs_status" nullable="true" length="32" comment="Cc Avs Status"/> <column xsi:type="varchar" name="cc_number_enc" nullable="true" length="128"/> - <column xsi:type="varchar" name="cc_trans_id" nullable="true" length="32" comment="Cc Trans Id"/> + <column xsi:type="varchar" name="cc_trans_id" nullable="true" length="32" comment="Cc Trans ID"/> <column xsi:type="varchar" name="address_status" nullable="true" length="32" comment="Address Status"/> <column xsi:type="text" name="additional_information" nullable="true" comment="Additional Information"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -696,9 +696,9 @@ </table> <table name="sales_shipment" resource="sales" engine="innodb" comment="Sales Flat Shipment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="decimal" name="total_weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Weight"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" @@ -708,16 +708,16 @@ <column xsi:type="smallint" name="send_email" padding="5" unsigned="true" nullable="true" identity="false" comment="Send Email"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="int" name="shipment_status" padding="11" unsigned="false" nullable="true" identity="false" comment="Shipment Status"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -762,15 +762,15 @@ </table> <table name="sales_shipment_grid" resource="sales" engine="innodb" comment="Sales Flat Shipment Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + comment="Entity ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> - <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment Id"/> + comment="Store ID"/> + <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="timestamp" name="order_created_at" on_update="true" nullable="false" - default="CURRENT_TIMESTAMP" comment="Order Increment Id"/> + default="CURRENT_TIMESTAMP" comment="Order Increment ID"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Qty"/> @@ -837,9 +837,9 @@ </table> <table name="sales_shipment_item" resource="sales" engine="innodb" comment="Sales Flat Shipment Item"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total"/> <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="true" @@ -848,9 +848,9 @@ comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Qty"/> <column xsi:type="int" name="product_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="order_item_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Order Item Id"/> + comment="Order Item ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> @@ -867,14 +867,14 @@ </table> <table name="sales_shipment_track" resource="sales" engine="innodb" comment="Sales Flat Shipment Track"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Qty"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="text" name="track_number" nullable="true" comment="Number"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> @@ -901,9 +901,9 @@ </table> <table name="sales_shipment_comment" resource="sales" engine="innodb" comment="Sales Flat Shipment Comment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="is_customer_notified" padding="11" unsigned="false" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -926,9 +926,9 @@ </table> <table name="sales_invoice" resource="sales" engine="innodb" comment="Sales Flat Invoice"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" @@ -968,11 +968,11 @@ <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="smallint" name="is_used_for_refund" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Used For Refund"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="smallint" name="email_sent" padding="5" unsigned="true" nullable="true" identity="false" comment="Email Sent"/> <column xsi:type="smallint" name="send_email" padding="5" unsigned="true" nullable="true" identity="false" @@ -982,14 +982,14 @@ <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="State"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> - <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction Id"/> + <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction ID"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="3" comment="Global Currency Code"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -1051,16 +1051,16 @@ </table> <table name="sales_invoice_grid" resource="sales" engine="innodb" comment="Sales Flat Invoice Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + comment="Entity ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="State"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> - <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment Id"/> + comment="Order ID"/> + <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment ID"/> <column xsi:type="timestamp" name="order_created_at" on_update="false" nullable="true" comment="Order Created At"/> <column xsi:type="varchar" name="customer_name" nullable="true" length="255" comment="Customer Name"/> @@ -1136,9 +1136,9 @@ </table> <table name="sales_invoice_item" resource="sales" engine="innodb" comment="Sales Flat Invoice Item"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="base_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Price"/> <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" @@ -1167,9 +1167,9 @@ <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> <column xsi:type="int" name="product_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="order_item_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Order Item Id"/> + comment="Order Item ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> @@ -1192,9 +1192,9 @@ </table> <table name="sales_invoice_comment" resource="sales" engine="innodb" comment="Sales Flat Invoice Comment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="smallint" name="is_customer_notified" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -1217,9 +1217,9 @@ </table> <table name="sales_creditmemo" resource="sales" engine="innodb" comment="Sales Flat Creditmemo"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" @@ -1269,7 +1269,7 @@ <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="smallint" name="email_sent" padding="5" unsigned="true" nullable="true" identity="false" comment="Email Sent"/> <column xsi:type="smallint" name="send_email" padding="5" unsigned="true" nullable="true" identity="false" @@ -1279,18 +1279,18 @@ <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="State"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="int" name="invoice_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Invoice Id"/> + comment="Invoice ID"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="3" comment="Global Currency Code"/> - <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -1350,13 +1350,13 @@ </table> <table name="sales_creditmemo_grid" resource="sales" engine="innodb" comment="Sales Flat Creditmemo Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + comment="Entity ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated At"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> - <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment Id"/> + comment="Order ID"/> + <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment ID"/> <column xsi:type="timestamp" name="order_created_at" on_update="false" nullable="true" comment="Order Created At"/> <column xsi:type="varchar" name="billing_name" nullable="true" length="255" comment="Billing Name"/> @@ -1366,13 +1366,13 @@ comment="Base Grand Total"/> <column xsi:type="varchar" name="order_status" nullable="true" length="32" comment="Order Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="billing_address" nullable="true" length="255" comment="Billing Address"/> <column xsi:type="varchar" name="shipping_address" nullable="true" length="255" comment="Shipping Address"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="128" comment="Customer Email"/> <column xsi:type="smallint" name="customer_group_id" padding="6" unsigned="false" nullable="true" - identity="false" comment="Customer Group Id"/> + identity="false" comment="Customer Group ID"/> <column xsi:type="varchar" name="payment_method" nullable="true" length="32" comment="Payment Method"/> <column xsi:type="varchar" name="shipping_information" nullable="true" length="255" comment="Shipping Method Name"/> @@ -1386,8 +1386,6 @@ comment="Adjustment Negative"/> <column xsi:type="decimal" name="order_base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Order Grand Total"/> - <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> - <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> @@ -1440,9 +1438,9 @@ </table> <table name="sales_creditmemo_item" resource="sales" engine="innodb" comment="Sales Flat Creditmemo Item"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="base_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Price"/> <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" @@ -1471,9 +1469,9 @@ <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> <column xsi:type="int" name="product_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="order_item_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Order Item Id"/> + comment="Order Item ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> @@ -1496,9 +1494,9 @@ </table> <table name="sales_creditmemo_comment" resource="sales" engine="innodb" comment="Sales Flat Creditmemo Comment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="is_customer_notified" padding="11" unsigned="false" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -1520,10 +1518,10 @@ </index> </table> <table name="sales_invoiced_aggregated" resource="sales" engine="innodb" comment="Sales Invoiced Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1552,10 +1550,10 @@ </table> <table name="sales_invoiced_aggregated_order" resource="sales" engine="innodb" comment="Sales Invoiced Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1584,10 +1582,10 @@ </table> <table name="sales_order_aggregated_created" resource="sales" engine="innodb" comment="Sales Order Aggregated Created"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1638,10 +1636,10 @@ </table> <table name="sales_order_aggregated_updated" resource="sales" engine="innodb" comment="Sales Order Aggregated Updated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1692,15 +1690,15 @@ </table> <table name="sales_payment_transaction" resource="sales" engine="innodb" comment="Sales Payment Transaction"> <column xsi:type="int" name="transaction_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Transaction Id"/> + comment="Transaction ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Order Id"/> + default="0" comment="Order ID"/> <column xsi:type="int" name="payment_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Payment Id"/> - <column xsi:type="varchar" name="txn_id" nullable="true" length="100" comment="Txn Id"/> - <column xsi:type="varchar" name="parent_txn_id" nullable="true" length="100" comment="Parent Txn Id"/> + default="0" comment="Payment ID"/> + <column xsi:type="varchar" name="txn_id" nullable="true" length="100" comment="Txn ID"/> + <column xsi:type="varchar" name="parent_txn_id" nullable="true" length="100" comment="Parent Txn ID"/> <column xsi:type="varchar" name="txn_type" nullable="true" length="15" comment="Txn Type"/> <column xsi:type="smallint" name="is_closed" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Is Closed"/> @@ -1732,10 +1730,10 @@ </index> </table> <table name="sales_refunded_aggregated" resource="sales" engine="innodb" comment="Sales Refunded Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1762,10 +1760,10 @@ </table> <table name="sales_refunded_aggregated_order" resource="sales" engine="innodb" comment="Sales Refunded Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1791,10 +1789,10 @@ </index> </table> <table name="sales_shipping_aggregated" resource="sales" engine="innodb" comment="Sales Shipping Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="shipping_description" nullable="true" length="255" comment="Shipping Description"/> @@ -1822,10 +1820,10 @@ </table> <table name="sales_shipping_aggregated_order" resource="sales" engine="innodb" comment="Sales Shipping Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="shipping_description" nullable="true" length="255" comment="Shipping Description"/> @@ -1853,12 +1851,12 @@ </table> <table name="sales_bestsellers_aggregated_daily" resource="sales" engine="innodb" comment="Sales Bestsellers Aggregated Daily"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -1886,12 +1884,12 @@ </table> <table name="sales_bestsellers_aggregated_monthly" resource="sales" engine="innodb" comment="Sales Bestsellers Aggregated Monthly"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -1919,12 +1917,12 @@ </table> <table name="sales_bestsellers_aggregated_yearly" resource="sales" engine="innodb" comment="Sales Bestsellers Aggregated Yearly"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -1952,9 +1950,9 @@ </table> <table name="sales_order_tax" resource="sales" engine="innodb" comment="Sales Order Tax Table"> <column xsi:type="int" name="tax_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Tax Id"/> + comment="Tax ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="decimal" name="percent" scale="4" precision="12" unsigned="false" nullable="true" @@ -1982,11 +1980,11 @@ </table> <table name="sales_order_tax_item" resource="sales" engine="innodb" comment="Sales Order Tax Item"> <column xsi:type="int" name="tax_item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Tax Item Id"/> + comment="Tax Item ID"/> <column xsi:type="int" name="tax_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Tax Id"/> + comment="Tax ID"/> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="false" comment="Real Tax Percent For Item"/> <column xsi:type="decimal" name="amount" scale="4" precision="20" unsigned="false" nullable="false" @@ -2046,7 +2044,7 @@ <table name="sales_order_status_label" resource="sales" engine="innodb" comment="Sales Order Status Label Table"> <column xsi:type="varchar" name="status" nullable="false" length="32" comment="Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="label" nullable="false" length="128" comment="Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status"/> diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json index a7215d08c1f10..087fe6c9eb5ac 100644 --- a/app/code/Magento/Sales/etc/db_schema_whitelist.json +++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json @@ -815,9 +815,7 @@ "shipping_and_handling": true, "adjustment_positive": true, "adjustment_negative": true, - "order_base_grand_total": true, - "order_currency_code": true, - "base_currency_code": true + "order_base_grand_total": true }, "index": { "SALES_CREDITMEMO_GRID_ORDER_INCREMENT_ID": true, @@ -1247,4 +1245,4 @@ "SALES_ORDER_STATUS_LABEL_STORE_ID_STORE_STORE_ID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index a0dbb0488acb3..f6618c9884d60 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -641,8 +641,6 @@ <item name="adjustment_positive" xsi:type="string">sales_creditmemo.adjustment_positive</item> <item name="adjustment_negative" xsi:type="string">sales_creditmemo.adjustment_negative</item> <item name="order_base_grand_total" xsi:type="string">sales_order.base_grand_total</item> - <item name="order_currency_code" xsi:type="string">sales_order.order_currency_code</item> - <item name="base_currency_code" xsi:type="string">sales_order.base_currency_code</item> </argument> </arguments> </virtualType> diff --git a/app/code/Magento/Sales/etc/module.xml b/app/code/Magento/Sales/etc/module.xml index e8af6b761a8a4..11eebaa3d5a3d 100644 --- a/app/code/Magento/Sales/etc/module.xml +++ b/app/code/Magento/Sales/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_Sales"> + <module name="Magento_Sales" > <sequence> <module name="Magento_Rule"/> <module name="Magento_Catalog"/> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index d09ee8376b2ed..f30315437533f 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -797,5 +797,5 @@ Created,Created Refunds,Refunds "Allow Zero GrandTotal for Creditmemo","Allow Zero GrandTotal for Creditmemo" "Allow Zero GrandTotal","Allow Zero GrandTotal" +"Could not save the shipment tracking","Could not save the shipment tracking" "Please enter a coupon code!","Please enter a coupon code!" - diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml index 71490553aff17..cd7ca1d7e0d42 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml @@ -18,6 +18,9 @@ </block> <block class="Magento\Sales\Block\Adminhtml\Order\Payment" name="order_payment"/> <block class="Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items" name="order_items" template="Magento_Sales::order/creditmemo/create/items.phtml"> + <arguments> + <argument name="viewModel" xsi:type="object">Magento\Sales\ViewModel\CreditMemo\Create\UpdateTotalsButton</argument> + </arguments> <block class="Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRenderer" name="order_items.default" as="default" template="Magento_Sales::order/creditmemo/create/items/renderer/default.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Qty" name="column_qty" template="Magento_Sales::items/column/qty.phtml" group="column"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Name" name="column_name" template="Magento_Sales::items/column/name.phtml" group="column"/> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml index 8375bec965794..94ef0bf9d7a03 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml @@ -9,6 +9,9 @@ <update handle="sales_order_item_price"/> <body> <block class="Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items" name="order_items" template="Magento_Sales::order/creditmemo/create/items.phtml"> + <arguments> + <argument name="viewModel" xsi:type="object">Magento\Sales\ViewModel\CreditMemo\Create\UpdateTotalsButton</argument> + </arguments> <block class="Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRenderer" name="order_items.default" as="default" template="Magento_Sales::order/creditmemo/create/items/renderer/default.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Qty" name="column_qty" template="Magento_Sales::items/column/qty.phtml" group="column"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Name" name="column_name" template="Magento_Sales::items/column/name.phtml" group="column"/> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml index 64600f31b47de..c9b2f7c8de254 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml @@ -29,7 +29,7 @@ <?php $_option = $block->getFormattedOption($_option['value']); ?> <?php $dots = 'dots' . uniqid(); ?> <?php $id = 'id' . uniqid(); ?> - <?= $block->escapeHtml($_option['value'], ['a']) ?><?php if (isset($_option['remainder']) && $_option['remainder']) : ?><span id="<?= /* @noEscape */ $dots; ?>"> ...</span><span id="<?= /* @noEscape */ $id; ?>"><?= $block->escapeHtml($_option['remainder'], ['a']) ?></span> + <?= $block->escapeHtml($_option['value'], ['a', 'br']) ?><?php if (isset($_option['remainder']) && $_option['remainder']) : ?><span id="<?= /* @noEscape */ $dots; ?>"> ...</span><span id="<?= /* @noEscape */ $id; ?>"><?= $block->escapeHtml($_option['remainder'], ['a']) ?></span> <script> require(['prototype'], function() { $('<?= /* @noEscape */ $id; ?>').hide(); diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index 31aefd8d2ca57..81dc778cff2df 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -6,7 +6,11 @@ /* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items $block */ ?> -<?php $_items = $block->getCreditmemo()->getAllItems() ?> +<?php +/** @var Magento\Sales\ViewModel\CreditMemo\Create\UpdateTotalsButton $viewModel */ +$viewModel = $block->getData('viewModel'); +$_items = $block->getCreditmemo()->getAllItems(); +?> <section class="admin__page-section"> <div class="admin__page-section-title"> @@ -34,11 +38,11 @@ <?php if ($block->canEditQty()) : ?> <tfoot> <tr> - <td colspan="3"> </td> - <td colspan="3"> + <td colspan="4"> </td> + <td> <?= $block->getUpdateButtonHtml() ?> </td> - <td colspan="3" class="last"> </td> + <td colspan="4" class="last"> </td> </tr> </tfoot> <?php endif; ?> @@ -100,7 +104,7 @@ <span class="title"><?= $block->escapeHtml(__('Refund Totals')) ?></span> </div> <?= $block->getChildHtml('creditmemo_totals') ?> - <div class="totals-actions"><?= $block->getUpdateTotalsButtonHtml() ?></div> + <div class="totals-actions"><?= /* @noEscape */ $viewModel->getUpdateTotalsButton() ?></div> <div class="order-totals-actions"> <div class="field choice admin__field admin__field-option field-append-comments"> <input id="notify_customer" @@ -140,9 +144,8 @@ require(['jquery'], function(jQuery){ //<![CDATA[ var submitButtons = jQuery('.submit-button'); -var updateButtons = jQuery('.update-button,.update-totals-button'); -var fields = jQuery('.qty-input,.order-subtotal-table input[type="text"]'); - +var updateButtons = jQuery('.update-button, .update-totals-button'); +var fields = jQuery('.qty-input, .order-subtotal-table input[type="text"]'); function enableButtons(buttons) { buttons.removeClass('disabled').prop('disabled', false); } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml index b700f1b3a65ad..70373f177d8be 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml @@ -81,8 +81,8 @@ $_order = $block->getOrder() ?> </tr> <?php endif; ?> <tr bgcolor="#DEE5E8"> - <td colspan="2" align="right" style="padding:3px 9px"><strong><big><?= $block->escapeHtml(__('Grand Total')) ?></big></strong></td> - <td align="right" style="padding:6px 9px"><strong><big><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></big></strong></td> + <td colspan="2" align="right" style="padding:3px 9px"><strong style="font-size: larger"><?= $block->escapeHtml(__('Grand Total')) ?></strong></td> + <td align="right" style="padding:6px 9px"><strong style="font-size: larger"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></strong></td> </tr> </tfoot> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml index 87d7c85c2d9ed..f8e914a2c9b2f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml @@ -6,7 +6,7 @@ ?> <?php if ($block->getCanDisplayTotalDue()) : ?> <tr> - <td class="label"><big><strong><?= $block->escapeHtml(__('Total Due')) ?></strong></big></td> - <td class="emph"><big><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></big></td> + <td class="label"><strong style="font-size: larger"><?= $block->escapeHtml(__('Total Due')) ?></strong></td> + <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml index dc76799251c7a..af5d58d47fce1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml @@ -9,13 +9,13 @@ <tr> <td class="label"> - <strong><big> + <strong style="font-size: larger"> <?php if ($block->getGrandTotalTitle()) : ?> <?= $block->escapeHtml($block->getGrandTotalTitle()) ?> <?php else : ?> <?= $block->escapeHtml(__('Grand Total')) ?> <?php endif; ?> - </big></strong> + </strong> </td> - <td class="emph"><big><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></big></td> + <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></td> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index ab5cd49449ece..825ac8205772e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -9,6 +9,10 @@ */ $order = $block->getOrder(); +$baseCurrencyCode = (string)$order->getBaseCurrencyCode(); +$globalCurrencyCode = (string)$order->getGlobalCurrencyCode(); +$orderCurrencyCode = (string)$order->getOrderCurrencyCode(); + $orderAdminDate = $block->formatDate( $block->getOrderAdminDate($order->getCreatedAt()), \IntlDateFormatter::MEDIUM, @@ -23,6 +27,7 @@ $orderStoreDate = $block->formatDate( ); $customerUrl = $block->getCustomerViewUrl(); + $allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul']; ?> @@ -93,15 +98,15 @@ $allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub <td><?= $block->escapeHtml($order->getRemoteIp()); ?><?= $order->getXForwardedFor() ? ' (' . $block->escapeHtml($order->getXForwardedFor()) . ')' : ''; ?></td> </tr> <?php endif; ?> - <?php if ($order->getGlobalCurrencyCode() != $order->getBaseCurrencyCode()) : ?> + <?php if ($globalCurrencyCode !== $baseCurrencyCode) : ?> <tr> - <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getGlobalCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> + <th><?= $block->escapeHtml(__('%1 / %2 rate:', $globalCurrencyCode, $baseCurrencyCode)) ?></th> <td><?= $block->escapeHtml($order->getBaseToGlobalRate()) ?></td> </tr> <?php endif; ?> - <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()) : ?> + <?php if ($baseCurrencyCode !== $orderCurrencyCode && $globalCurrencyCode !== $orderCurrencyCode) : ?> <tr> - <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getOrderCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> + <th><?= $block->escapeHtml(__('%1 / %2 rate:', $orderCurrencyCode, $baseCurrencyCode)) ?></th> <td><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml index 98f8b1edecf34..e0b7dae8fdb1a 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml @@ -119,7 +119,7 @@ <column name="base_grand_total" class="Magento\Sales\Ui\Component\Listing\Column\Price"> <settings> <filter>textRange</filter> - <label translate="true">Refunded (Base)</label> + <label translate="true">Refunded</label> </settings> </column> <column name="order_status" component="Magento_Ui/js/grid/columns/select"> @@ -194,7 +194,7 @@ <visible>false</visible> </settings> </column> - <column name="subtotal" class="Magento\Sales\Ui\Component\Listing\Column\PurchasedPrice"> + <column name="subtotal" class="Magento\Sales\Ui\Component\Listing\Column\Price"> <settings> <filter>textRange</filter> <label translate="true">Subtotal</label> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml index c0ed1e01460bc..ac1233c5e4961 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml @@ -130,7 +130,7 @@ <label translate="true">Status</label> </settings> </column> - <column name="base_grand_total" class="Magento\Sales\Ui\Component\Listing\Column\Price"> + <column name="grand_total" class="Magento\Sales\Ui\Component\Listing\Column\PurchasedPrice"> <settings> <filter>textRange</filter> <label translate="true">Amount</label> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 3fe9d08782880..4e07414510748 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -795,6 +795,20 @@ define([ grid.reloadParams = {'products[]':this.gridProducts.keys()}; }, + productGridFilterKeyPress: function (grid, event) { + var returnKey = parseInt(Event.KEY_RETURN || 13, 10); + + if (event.keyCode === returnKey) { + if (typeof event.stopPropagation === 'function') { + event.stopPropagation(); + } + + if (typeof event.preventDefault === 'function') { + event.preventDefault(); + } + } + }, + /** * Submit configured products to quote */ diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html index ca89446a2f7c0..5ae6f5f9d82c7 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html @@ -4,28 +4,34 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours", +"var creditmemo":"Credit Memo", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -56,7 +62,7 @@ <h1>{{trans "Your Credit Memo #%creditmemo_id for Order #%order_id" creditmemo_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html index b21f659814368..657de2aae2045 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html @@ -4,27 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.getName()":"Guest Customer Name (Billing)", +"var billing.name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var creditmemo":"Credit Memo", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} {{trans 'Our hours are <span class="no-link">%store_hours</span>.' store_hours=$store_hours |raw}} @@ -54,7 +60,7 @@ <h1>{{trans "Your Credit Memo #%creditmemo_id for Order #%order_id" creditmemo_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -66,10 +72,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html index a6a10fb49e3f5..7e7930f33f1b9 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.frontend_name}} @--> <!--@vars { -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html index b7411d80d2ba6..ed8f592b59638 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.frontend_name}} @--> <!--@vars { -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.getName()":"Guest Customer Name", +"var billing.name":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_new.html b/app/code/Magento/Sales/view/frontend/email/invoice_new.html index ca5f7ee632e22..68773ee9d7570 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_new.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_new.html @@ -4,28 +4,34 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Invoice Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout area=\"frontend\" handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var invoice": "Invoice", +"var order": "Order", +"var order_data.is_not_virtual": "Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -56,7 +62,7 @@ <h1>{{trans "Your Invoice #%invoice_id for Order #%order_id" invoice_id=$invoice <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html index c93df9f9e8efb..5053ccc2ac635 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html @@ -4,27 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.getName()":"Guest Customer Name", -"var comment":"Invoice Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var invoice": "Invoice", +"var order": "Order", +"var order_data.is_not_virtual": "Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} {{trans 'Our hours are <span class="no-link">%store_hours</span>.' store_hours=$store_hours |raw}} @@ -54,7 +60,7 @@ <h1>{{trans "Your Invoice #%invoice_id for Order #%order_id" invoice_id=$invoice <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -66,10 +72,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update.html b/app/code/Magento/Sales/view/frontend/email/invoice_update.html index 4043e59f9d7d6..a8f98a238e314 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Invoice Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html index 40cdec7fb4cab..289c5113fe285 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Invoice Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_new.html b/app/code/Magento/Sales/view/frontend/email/order_new.html index 370bdb0f2f336..13c436b131b82 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new.html @@ -4,16 +4,25 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var order.getEmailCustomerNote()":"Email Order Note", +"var order_data.email_customer_note|escape|nl2br":"Email Order Note", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order area=\"frontend\"":"Order Items Grid", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var shipping_msg":"Shipping message" +"var order.shipping_description":"Shipping Description", +"var shipping_msg":"Shipping message", +"var created_at_formatted":"Order Created At (datetime)", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.is_not_virtual":"Order Type", +"var order":"Order", +"var order_data.customer_name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} @@ -21,9 +30,9 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%customer_name," customer_name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%customer_name," customer_name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> @@ -38,16 +47,16 @@ <tr class="email-summary"> <td> <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_id=$order.increment_id |raw}}</h1> - <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$order.getCreatedAtFormatted(2) |raw}}</p> + <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$created_at_formatted |raw}}</p> </td> </tr> <tr class="email-information"> <td> - {{depend order.getEmailCustomerNote()}} + {{depend order_data.email_customer_note}} <table class="message-info"> <tr> <td> - {{var order.getEmailCustomerNote()|escape|nl2br}} + {{var order_data.email_customer_note|escape|nl2br}} </td> </tr> </table> @@ -58,7 +67,7 @@ <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -70,10 +79,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> {{if shipping_msg}} <p>{{var shipping_msg}}</p> {{/if}} diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html index cfd99e5b0936e..866a1ad87f9b1 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html @@ -4,27 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var order.getEmailCustomerNote()":"Email Order Note", -"var order.getBillingAddress().getName()":"Guest Customer Name", -"var order.getCreatedAtFormatted(2)":"Order Created At (datetime)", +"var order_data.email_customer_note|escape|nl2br":"Email Order Note", +"var order.billing_address.name":"Guest Customer Name", +"var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var shipping_msg":"Shipping message" +"var order.shipping_description":"Shipping Description", +"var shipping_msg":"Shipping message", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var order_data.is_not_virtual":"Order Type", +"var order":"Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getBillingAddress().getName()}}</p> + <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send an email with a link to track your order."}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -36,16 +42,16 @@ <tr class="email-summary"> <td> <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_id=$order.increment_id |raw}}</h1> - <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$order.getCreatedAtFormatted(2) |raw}}</p> + <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$created_at_formatted |raw}}</p> </td> </tr> <tr class="email-information"> <td> - {{depend order.getEmailCustomerNote()}} + {{depend order_data.email_customer_note}} <table class="message-info"> <tr> <td> - {{var order.getEmailCustomerNote()|escape|nl2br}} + {{var order_data.email_customer_note|escape|nl2br}} </td> </tr> </table> @@ -56,7 +62,7 @@ <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> {{if shipping_msg}} <p>{{var shipping_msg}}</p> {{/if}} diff --git a/app/code/Magento/Sales/view/frontend/email/order_update.html b/app/code/Magento/Sales/view/frontend/email/order_update.html index a8f0068b70e87..b2c4e86654f6f 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Order Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html index 749fa3b60ad59..1ce0d162ed76e 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html @@ -4,25 +4,29 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Order Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new.html b/app/code/Magento/Sales/view/frontend/email/shipment_new.html index 84f5acb29ea3b..39823a0c9d80b 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new.html @@ -4,29 +4,35 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", -"var comment":"Shipment Comment", +"var comment|escape|nl2br":"Shipment Comment", "var shipment.increment_id":"Shipment Id", "layout handle=\"sales_email_order_shipment_items\" shipment=$shipment order=$order":"Shipment Items Grid", "block class='Magento\\\\Framework\\\\View\\\\Element\\\\Template' area='frontend' template='Magento_Sales::email\/shipment\/track.phtml' shipment=$shipment order=$order":"Shipment Track Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var order_data.is_not_virtual": "Order Type", +"var shipment": "Shipment", +"var order": "Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -60,7 +66,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -72,10 +78,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html index bb181126724da..ed2f52ed85066 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -4,28 +4,34 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.getName()":"Guest Customer Name", +"var billing.name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", -"var comment":"Shipment Comment", +"var comment|escape|nl2br":"Shipment Comment", "var shipment.increment_id":"Shipment Id", "layout handle=\"sales_email_order_shipment_items\" shipment=$shipment order=$order":"Shipment Items Grid", "block class='Magento\\\\Framework\\\\View\\\\Element\\\\Template' area='frontend' template='Magento_Sales::email\/shipment\/track.phtml' shipment=$shipment order=$order":"Shipment Track Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var order_data.is_not_virtual": "Order Type", +"var shipment": "Shipment", +"var order": "Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} {{trans 'Our hours are <span class="no-link">%store_hours</span>.' store_hours=$store_hours |raw}} @@ -58,7 +64,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -70,10 +76,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update.html b/app/code/Magento/Sales/view/frontend/email/shipment_update.html index 9d1c93287549a..9d0057f78df7f 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Order Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status", -"var shipment.increment_id":"Shipment Id" +"var order_data.frontend_status_label":"Order Status", +"var shipment.increment_id":"Shipment Id", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html index 0d2dccd3377d2..087cb0ddbf5bc 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Order Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status", -"var shipment.increment_id":"Shipment Id" +"var order_data.frontend_status_label":"Order Status", +"var shipment.increment_id":"Shipment Id", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml index 019baeea54e23..cb84dcc3fae85 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml @@ -7,8 +7,9 @@ <?php $_order = $block->getOrder() ?> <div class="actions-toolbar"> <a href="<?= $block->escapeUrl($block->getPrintAllCreditmemosUrl($_order)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print All Refunds')) ?></span> </a> </div> @@ -16,8 +17,9 @@ <div class="order-title"> <strong><?= $block->escapeHtml(__('Refund #')) ?><?= $block->escapeHtml($_creditmemo->getIncrementId()) ?> </strong> <a href="<?= $block->escapeUrl($block->getPrintCreditmemoUrl($_creditmemo)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Refund')) ?></span> </a> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml index 9c0bf0182c62e..b2e84691a45cf 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml @@ -17,7 +17,7 @@ <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml index ef56ad69dcb1b..a785ca93511ad 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml @@ -5,6 +5,7 @@ */ // phpcs:disable Magento2.Templates.ThisInTemplate +// @codingStandardsIgnoreFile /** @var \Magento\Sales\Block\Order\History $block */ ?> @@ -19,7 +20,6 @@ <th scope="col" class="col id"><?= $block->escapeHtml(__('Order #')) ?></th> <th scope="col" class="col date"><?= $block->escapeHtml(__('Date')) ?></th> <?= $block->getChildHtml('extra.column.header') ?> - <th scope="col" class="col shipping"><?= $block->escapeHtml(__('Ship To')) ?></th> <th scope="col" class="col total"><?= $block->escapeHtml(__('Order Total')) ?></th> <th scope="col" class="col status"><?= $block->escapeHtml(__('Status')) ?></th> <th scope="col" class="col actions"><?= $block->escapeHtml(__('Action')) ?></th> @@ -35,7 +35,6 @@ <?php $extra->setOrder($_order); ?> <?= $extra->getChildHtml() ?> <?php endif; ?> - <td data-th="<?= $block->escapeHtml(__('Ship To')) ?>" class="col shipping"><?= $_order->getShippingAddress() ? $block->escapeHtml($_order->getShippingAddress()->getName()) : ' ' ?></td> <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></td> <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= $block->escapeHtml($_order->getStatusLabel()) ?></td> <td data-th="<?= $block->escapeHtml(__('Actions')) ?>" class="col actions"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml index 6b87d3c22331c..2872291a0eaad 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml @@ -16,9 +16,10 @@ <span><?= $block->escapeHtml(__('Reorder')) ?></span> </a> <?php endif ?> - <a class="action print" - href="<?= $block->escapeUrl($block->getPrintUrl($_order)) ?>" - onclick="this.target='_blank';"> + <a href="<?= $block->escapeUrl($block->getPrintUrl($_order)) ?>" + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Order')) ?></span> </a> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml index 419060bfba713..ba3440f03c00f 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml @@ -7,8 +7,9 @@ <?php $_order = $block->getOrder() ?> <div class="actions-toolbar"> <a href="<?= $block->escapeUrl($block->getPrintAllInvoicesUrl($_order)) ?>" + class="action print" target="_blank" - class="action print"> + rel="noopener"> <span><?= $block->escapeHtml(__('Print All Invoices')) ?></span> </a> </div> @@ -16,8 +17,9 @@ <div class="order-title"> <strong><?= $block->escapeHtml(__('Invoice #')) ?><?= $block->escapeHtml($_invoice->getIncrementId()) ?></strong> <a href="<?= $block->escapeUrl($block->getPrintInvoiceUrl($_invoice)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Invoice')) ?></span> </a> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml index 1c427e8b6d4e2..0176582f0fcd7 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml @@ -17,7 +17,7 @@ <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml index 4042fe52bb5a8..51e43476238be 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml @@ -16,17 +16,19 @@ $_item = $block->getItem(); <dt><?= $block->escapeHtml($_option['label']) ?></dt> <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd> + <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> - <?= $block->escapeHtml($_formatedOptionValue['full_view'], ['a']) ?> - <?php else : ?> - <?=$block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <div class="tooltip content"> + <dl class="item options"> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dd><?= $block->escapeHtml($_formatedOptionValue['full_view']) ?></dd> + </dl> + </div> <?php endif; ?> </dd> <?php else : ?> - <dd> - <?= /* @noEscape */ nl2br($block->escapeHtml($_option['print_value'] ?? $_option['value'])) ?> - </dd> + <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml index 57aeffb26f823..26fe74b0fc454 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml @@ -16,7 +16,7 @@ <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php index 4fd06e88878b4..8d81afeab4c90 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -56,6 +56,7 @@ public function resolve( $items[] = [ 'id' => $order->getId(), 'increment_id' => $order->getIncrementId(), + 'order_number' => $order->getIncrementId(), 'created_at' => $order->getCreatedAt(), 'grand_total' => $order->getGrandTotal(), 'status' => $order->getStatus(), diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 06146f805c644..a687ee59031ea 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -7,7 +7,8 @@ type Query { type CustomerOrder @doc(description: "Order mapping fields") { id: Int - increment_id: String + increment_id: String @deprecated(reason: "Use the order_number instead.") + order_number: String! @doc(description: "The order number") created_at: String grand_total: Float status: String diff --git a/app/code/Magento/SalesRule/Api/Data/DiscountDataInterface.php b/app/code/Magento/SalesRule/Api/Data/DiscountDataInterface.php new file mode 100644 index 0000000000000..38ac93dc9762f --- /dev/null +++ b/app/code/Magento/SalesRule/Api/Data/DiscountDataInterface.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Api\Data; + +/** + * Discount Data Interface + */ +interface DiscountDataInterface +{ + /** + * Get Amount + * + * @return float + */ + public function getAmount(); + + /** + * Get Base Amount + * + * @return float + */ + public function getBaseAmount(); + + /** + * Get Original Amount + * + * @return float + */ + public function getOriginalAmount(); + + /** + * Get Base Original Amount + * + * @return float + */ + public function getBaseOriginalAmount(); +} diff --git a/app/code/Magento/SalesRule/Api/Data/RuleDiscountInterface.php b/app/code/Magento/SalesRule/Api/Data/RuleDiscountInterface.php new file mode 100644 index 0000000000000..8d2b73476af46 --- /dev/null +++ b/app/code/Magento/SalesRule/Api/Data/RuleDiscountInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Api\Data; + +/** + * Rule discount Interface + */ +interface RuleDiscountInterface +{ + /** + * Get Discount Data + * + * @return \Magento\SalesRule\Api\Data\DiscountDataInterface + */ + public function getDiscountData(); + + /** + * Get Rule Label + * + * @return string + */ + public function getRuleLabel(); + + /** + * Get Rule ID + * + * @return int + */ + public function getRuleID(); +} diff --git a/app/code/Magento/SalesRule/Api/Data/RuleInterface.php b/app/code/Magento/SalesRule/Api/Data/RuleInterface.php index 34341e16cfb64..94e6ed8b4584a 100644 --- a/app/code/Magento/SalesRule/Api/Data/RuleInterface.php +++ b/app/code/Magento/SalesRule/Api/Data/RuleInterface.php @@ -5,13 +5,15 @@ */ namespace Magento\SalesRule\Api\Data; +use Magento\Framework\Api\ExtensibleDataInterface; + /** * Interface RuleInterface * * @api * @since 100.0.2 */ -interface RuleInterface extends \Magento\Framework\Api\ExtensibleDataInterface +interface RuleInterface extends ExtensibleDataInterface { const FREE_SHIPPING_NONE = 'NONE'; const FREE_SHIPPING_MATCHING_ITEMS_ONLY = 'MATCHING_ITEMS_ONLY'; @@ -173,7 +175,7 @@ public function getIsActive(); * Set whether the coupon is active * * @param bool $isActive - * @return bool + * @return $this */ public function setIsActive($isActive); @@ -232,6 +234,8 @@ public function setStopRulesProcessing($stopRulesProcessing); public function getIsAdvanced(); /** + * Set if rule is advanced + * * @param bool $isAdvanced * @return $this */ @@ -260,6 +264,8 @@ public function setProductIds(array $productIds = null); public function getSortOrder(); /** + * Set sort order + * * @param int $sortOrder * @return $this */ @@ -446,5 +452,5 @@ public function getExtensionAttributes(); * @param \Magento\SalesRule\Api\Data\RuleExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes(\Magento\SalesRule\Api\Data\RuleExtensionInterface $extensionAttributes); + public function setExtensionAttributes(RuleExtensionInterface $extensionAttributes); } diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php index bf92e21827a01..b021b3e539a9d 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php @@ -100,7 +100,7 @@ protected function _prepareColumns() $this->addColumn( 'used', [ - 'header' => __('Uses'), + 'header' => __('Used'), 'index' => 'times_used', 'width' => '100', 'type' => 'options', diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php index 89a0d6e579727..56c08864c90c4 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php @@ -1,12 +1,19 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class NewActionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Rule\Model\Condition\AbstractCondition; +use Magento\SalesRule\Controller\Adminhtml\Promo\Quote; +use Magento\SalesRule\Model\Rule; + +/** + * New action html action + */ +class NewActionHtml extends Quote implements HttpPostActionInterface { /** * New action html action @@ -15,8 +22,10 @@ class NewActionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote */ public function execute() { - $id = $this->getRequest()->getParam('id'); - $formName = $this->getRequest()->getParam('form'); + $id = $this->getRequest() + ->getParam('id'); + $formName = $this->getRequest() + ->getParam('form_namespace'); $typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type'))); $type = $typeArr[0]; @@ -27,7 +36,7 @@ public function execute() )->setType( $type )->setRule( - $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class) + $this->_objectManager->create(Rule::class) )->setPrefix( 'actions' ); @@ -35,12 +44,14 @@ public function execute() $model->setAttribute($typeArr[1]); } - if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) { + if ($model instanceof AbstractCondition) { $model->setJsFormObject($formName); + $model->setFormName($formName); $html = $model->asHtmlRecursive(); } else { $html = ''; } - $this->getResponse()->setBody($html); + $this->getResponse() + ->setBody($html); } } diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php index 7d55d18b770e2..3513331913492 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php @@ -61,7 +61,8 @@ public function __construct( */ public function execute() { - if ($this->getRequest()->getPostValue()) { + $data = $this->getRequest()->getPostValue(); + if ($data) { try { /** @var $model \Magento\SalesRule\Model\Rule */ $model = $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class); @@ -69,7 +70,6 @@ public function execute() 'adminhtml_controller_salesrule_prepare_save', ['request' => $this->getRequest()] ); - $data = $this->getRequest()->getPostValue(); if (empty($data['from_date'])) { $data['from_date'] = $this->timezone->formatDate(); } diff --git a/app/code/Magento/SalesRule/Model/CouponSearchResult.php b/app/code/Magento/SalesRule/Model/CouponSearchResult.php new file mode 100644 index 0000000000000..cba57900cf605 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/CouponSearchResult.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\SalesRule\Api\Data\CouponSearchResultInterface; + +/** + * Service Data Object with Coupon search results. + * + * @phpcs:ignoreFile + */ +class CouponSearchResult extends SearchResults implements CouponSearchResultInterface +{ + /** + * @inheritdoc + */ + public function setItems(array $items = null) + { + return parent::setItems($items); + } +} diff --git a/app/code/Magento/SalesRule/Model/Data/DiscountData.php b/app/code/Magento/SalesRule/Model/Data/DiscountData.php new file mode 100644 index 0000000000000..cfad4b5c09c55 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Data/DiscountData.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Data; + +use Magento\SalesRule\Api\Data\DiscountDataInterface; +use Magento\Framework\Api\ExtensionAttributesInterface; + +/** + * Discount Data Model + */ +class DiscountData extends \Magento\Framework\Api\AbstractExtensibleObject implements DiscountDataInterface +{ + + const AMOUNT = 'amount'; + const BASE_AMOUNT = 'base_amount'; + const ORIGINAL_AMOUNT = 'original_amount'; + const BASE_ORIGINAL_AMOUNT = 'base_original_amount'; + + /** + * Get Amount + * + * @return float + */ + public function getAmount() + { + return $this->_get(self::AMOUNT); + } + + /** + * Set Amount + * + * @param float $amount + * @return $this + */ + public function setAmount(float $amount) + { + return $this->setData(self::AMOUNT, $amount); + } + + /** + * Get Base Amount + * + * @return float + */ + public function getBaseAmount() + { + return $this->_get(self::BASE_AMOUNT); + } + + /** + * Set Base Amount + * + * @param float $amount + * @return $this + */ + public function setBaseAmount(float $amount) + { + return $this->setData(self::BASE_AMOUNT, $amount); + } + + /** + * Get Original Amount + * + * @return float + */ + public function getOriginalAmount() + { + return $this->_get(self::ORIGINAL_AMOUNT); + } + + /** + * Set Original Amount + * + * @param float $amount + * @return $this + */ + public function setOriginalAmount(float $amount) + { + return $this->setData(self::ORIGINAL_AMOUNT, $amount); + } + + /** + * Get Base Original Amount + * + * @return float + */ + public function getBaseOriginalAmount() + { + return $this->_get(self::BASE_ORIGINAL_AMOUNT); + } + + /** + * Set Base Original Amount + * + * @param float $amount + * @return $this + */ + public function setBaseOriginalAmount(float $amount) + { + return $this->setData(self::BASE_ORIGINAL_AMOUNT, $amount); + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return ExtensionAttributesInterface|null + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * Set an extension attributes object. + * + * @param ExtensionAttributesInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + ExtensionAttributesInterface $extensionAttributes + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/SalesRule/Model/Data/Rule.php b/app/code/Magento/SalesRule/Model/Data/Rule.php index 58520831c016b..869822ab917cd 100644 --- a/app/code/Magento/SalesRule/Model/Data/Rule.php +++ b/app/code/Magento/SalesRule/Model/Data/Rule.php @@ -5,10 +5,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\SalesRule\Model\Data; +use Magento\Framework\Api\AbstractExtensibleObject; use Magento\SalesRule\Api\Data\ConditionInterface; +use Magento\SalesRule\Api\Data\RuleExtensionInterface; use Magento\SalesRule\Api\Data\RuleInterface; +use Magento\SalesRule\Api\Data\RuleLabelInterface; /** * Class Rule @@ -16,7 +21,7 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @codeCoverageIgnore */ -class Rule extends \Magento\Framework\Api\AbstractExtensibleObject implements RuleInterface +class Rule extends AbstractExtensibleObject implements RuleInterface { const KEY_RULE_ID = 'rule_id'; const KEY_NAME = 'name'; @@ -187,7 +192,7 @@ public function getIsActive() * Set whether the coupon is active * * @param bool $isActive - * @return bool + * @return $this */ public function setIsActive($isActive) { @@ -197,7 +202,7 @@ public function setIsActive($isActive) /** * Get condition for the rule * - * @return \Magento\SalesRule\Api\Data\ConditionInterface|null + * @return ConditionInterface|null */ public function getCondition() { @@ -207,7 +212,7 @@ public function getCondition() /** * Set condition for the rule * - * @param \Magento\SalesRule\Api\Data\ConditionInterface|null $condition + * @param ConditionInterface|null $condition * @return $this */ public function setCondition(ConditionInterface $condition = null) @@ -218,7 +223,7 @@ public function setCondition(ConditionInterface $condition = null) /** * Get action condition * - * @return \Magento\SalesRule\Api\Data\ConditionInterface|null + * @return ConditionInterface|null */ public function getActionCondition() { @@ -228,7 +233,7 @@ public function getActionCondition() /** * Set action condition * - * @param \Magento\SalesRule\Api\Data\ConditionInterface|null $actionCondition + * @param ConditionInterface|null $actionCondition * @return $this */ public function setActionCondition(ConditionInterface $actionCondition = null) @@ -283,7 +288,7 @@ public function setIsAdvanced($isAdvanced) /** * Get display label * - * @return \Magento\SalesRule\Api\Data\RuleLabelInterface[]|null + * @return RuleLabelInterface[]|null */ public function getStoreLabels() { @@ -293,7 +298,7 @@ public function getStoreLabels() /** * Set display label * - * @param \Magento\SalesRule\Api\Data\RuleLabelInterface[]|null $storeLabels + * @param RuleLabelInterface[]|null $storeLabels * @return $this */ public function setStoreLabels(array $storeLabels = null) @@ -622,7 +627,7 @@ public function setSimpleFreeShipping($simpleFreeShipping) /** * @inheritdoc * - * @return \Magento\SalesRule\Api\Data\RuleExtensionInterface|null + * @return RuleExtensionInterface|null */ public function getExtensionAttributes() { @@ -632,11 +637,11 @@ public function getExtensionAttributes() /** * @inheritdoc * - * @param \Magento\SalesRule\Api\Data\RuleExtensionInterface $extensionAttributes + * @param RuleExtensionInterface $extensionAttributes * @return $this */ public function setExtensionAttributes( - \Magento\SalesRule\Api\Data\RuleExtensionInterface $extensionAttributes + RuleExtensionInterface $extensionAttributes ) { return $this->_setExtensionAttributes($extensionAttributes); } diff --git a/app/code/Magento/SalesRule/Model/Data/RuleDiscount.php b/app/code/Magento/SalesRule/Model/Data/RuleDiscount.php new file mode 100644 index 0000000000000..aaf657c06b4fc --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Data/RuleDiscount.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Data; + +use Magento\Framework\Api\ExtensionAttributesInterface; +use Magento\SalesRule\Api\Data\RuleDiscountInterface; +use Magento\Framework\Api\AbstractExtensibleObject; + +/** + * Data Model for Rule Discount + */ +class RuleDiscount extends AbstractExtensibleObject implements RuleDiscountInterface +{ + const KEY_DISCOUNT_DATA = 'discount'; + const KEY_RULE_LABEL = 'rule'; + const KEY_RULE_ID = 'rule_id'; + + /** + * Get Discount Data + * + * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data + */ + public function getDiscountData() + { + return $this->_get(self::KEY_DISCOUNT_DATA); + } + + /** + * Get Rule Label + * + * @return string + */ + public function getRuleLabel() + { + return $this->_get(self::KEY_RULE_LABEL); + } + + /** + * Get Rule ID + * + * @return int + */ + public function getRuleID() + { + return $this->_get(self::KEY_RULE_ID); + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return ExtensionAttributesInterface|null + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * Set an extension attributes object. + * + * @param ExtensionAttributesInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + ExtensionAttributesInterface $extensionAttributes + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 315ce874513a3..69abac8309f90 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -5,6 +5,10 @@ */ namespace Magento\SalesRule\Model\Quote; +use Magento\Framework\App\ObjectManager; +use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; +use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; + /** * Discount totals calculation model. */ @@ -36,23 +40,41 @@ class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal */ protected $priceCurrency; + /** + * @var RuleDiscountInterfaceFactory + */ + private $discountInterfaceFactory; + + /** + * @var DiscountDataInterfaceFactory + */ + private $discountDataInterfaceFactory; + /** * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\SalesRule\Model\Validator $validator * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param RuleDiscountInterfaceFactory|null $discountInterfaceFactory + * @param DiscountDataInterfaceFactory|null $discountDataInterfaceFactory */ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\SalesRule\Model\Validator $validator, - \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + RuleDiscountInterfaceFactory $discountInterfaceFactory = null, + DiscountDataInterfaceFactory $discountDataInterfaceFactory = null ) { $this->setCode(self::COLLECTOR_TYPE_CODE); $this->eventManager = $eventManager; $this->calculator = $validator; $this->storeManager = $storeManager; $this->priceCurrency = $priceCurrency; + $this->discountInterfaceFactory = $discountInterfaceFactory + ?: ObjectManager::getInstance()->get(RuleDiscountInterfaceFactory::class); + $this->discountDataInterfaceFactory = $discountDataInterfaceFactory + ?: ObjectManager::getInstance()->get(DiscountDataInterfaceFactory::class); } /** @@ -91,6 +113,8 @@ public function collect( $address->setDiscountDescription([]); $items = $this->calculator->sortItemsByPriority($items, $address); + $address->getExtensionAttributes()->setDiscounts([]); + $addressDiscountAggregator = []; /** @var \Magento\Quote\Model\Quote\Item $item */ foreach ($items as $item) { @@ -127,13 +151,15 @@ public function collect( $this->calculator->process($item); $this->aggregateItemDiscount($item, $total); } + if ($item->getExtensionAttributes()) { + $this->aggregateDiscountPerRule($item, $address, $addressDiscountAggregator); + } } $this->calculator->prepareDescription($address); $total->setDiscountDescription($address->getDiscountDescription()); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); - $address->setDiscountAmount($total->getDiscountAmount()); $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); @@ -218,4 +244,56 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu } return $result; } + + /** + * Aggregates discount per rule + * + * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param \Magento\Quote\Api\Data\AddressInterface $address + * @param array $addressDiscountAggregator + * @return void + */ + private function aggregateDiscountPerRule( + \Magento\Quote\Model\Quote\Item\AbstractItem $item, + \Magento\Quote\Api\Data\AddressInterface $address, + array &$addressDiscountAggregator + ) { + $discountBreakdown = $item->getExtensionAttributes()->getDiscounts(); + if ($discountBreakdown) { + foreach ($discountBreakdown as $value) { + /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discount */ + $discount = $value->getDiscountData(); + $ruleLabel = $value->getRuleLabel(); + $ruleID = $value->getRuleID(); + if (isset($addressDiscountAggregator[$ruleID])) { + /** @var \Magento\SalesRule\Model\Data\RuleDiscount $cartDiscount */ + $cartDiscount = $addressDiscountAggregator[$ruleID]; + $discountData = $cartDiscount->getDiscountData(); + $discountData->setBaseAmount($discountData->getBaseAmount()+$discount->getBaseAmount()); + $discountData->setAmount($discountData->getAmount()+$discount->getAmount()); + $discountData->setOriginalAmount($discountData->getOriginalAmount()+$discount->getOriginalAmount()); + $discountData->setBaseOriginalAmount( + $discountData->getBaseOriginalAmount()+$discount->getBaseOriginalAmount() + ); + } else { + $data = [ + 'amount' => $discount->getAmount(), + 'base_amount' => $discount->getBaseAmount(), + 'original_amount' => $discount->getOriginalAmount(), + 'base_original_amount' => $discount->getBaseOriginalAmount() + ]; + $discountData = $this->discountDataInterfaceFactory->create(['data' => $data]); + $data = [ + 'discount' => $discountData, + 'rule' => $ruleLabel, + 'rule_id' => $ruleID, + ]; + /** @var \Magento\SalesRule\Model\Data\RuleDiscount $cartDiscount */ + $cartDiscount = $this->discountInterfaceFactory->create(['data' => $data]); + $addressDiscountAggregator[$ruleID] = $cartDiscount; + } + } + } + $address->getExtensionAttributes()->setDiscounts(array_values($addressDiscountAggregator)); + } } diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/Data.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/Data.php index fe564690e08b8..f9892fb48547c 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/Data.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/Data.php @@ -6,6 +6,8 @@ namespace Magento\SalesRule\Model\Rule\Action\Discount; /** + * Discount Data + * * @api * @since 100.0.2 */ @@ -43,6 +45,8 @@ public function __construct() } /** + * Set Amount + * * @param float $amount * @return $this */ @@ -53,6 +57,8 @@ public function setAmount($amount) } /** + * Get Amount + * * @return float */ public function getAmount() @@ -61,6 +67,8 @@ public function getAmount() } /** + * Set Base Amount + * * @param float $baseAmount * @return $this */ @@ -71,6 +79,8 @@ public function setBaseAmount($baseAmount) } /** + * Get Base Amount + * * @return float */ public function getBaseAmount() @@ -79,6 +89,8 @@ public function getBaseAmount() } /** + * Set Original Amount + * * @param float $originalAmount * @return $this */ @@ -99,6 +111,8 @@ public function getOriginalAmount() } /** + * Set Base Original Amount + * * @param float $baseOriginalAmount * @return $this */ diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php b/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php new file mode 100644 index 0000000000000..a0fd4bf576f61 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Rule\Action; + +use Magento\Framework\Data\OptionSourceInterface; +use Magento\SalesRule\Model\Rule; + +/** + * Class SimpleActionOptionsProvider + */ +class SimpleActionOptionsProvider implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray() + { + return [ + ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION], + ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION], + ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION], + ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION] + ]; + } +} diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index cf6301cb31a9c..29cdf34c5a784 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -65,7 +65,6 @@ public function loadAttributeOptions() 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), - 'payment_method' => __('Payment Method'), 'shipping_method' => __('Shipping Method'), 'postcode' => __('Shipping Postcode'), 'region' => __('Shipping Region'), diff --git a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php index fdd6c2b169a7d..e4aaaec98dc79 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php +++ b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php @@ -5,11 +5,14 @@ */ namespace Magento\SalesRule\Model\Rule\Metadata; -use Magento\SalesRule\Model\Rule; -use Magento\Store\Model\System\Store; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Convert\DataObject; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider; +use Magento\SalesRule\Model\RuleFactory; +use Magento\Store\Model\System\Store; /** * Metadata provider for sales rule edit form. @@ -37,10 +40,15 @@ class ValueProvider protected $objectConverter; /** - * @var \Magento\SalesRule\Model\RuleFactory + * @var RuleFactory */ protected $salesRuleFactory; + /** + * @var SimpleActionOptionsProvider + */ + private $simpleActionOptionsProvider; + /** * Initialize dependencies. * @@ -48,20 +56,24 @@ class ValueProvider * @param GroupRepositoryInterface $groupRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param DataObject $objectConverter - * @param \Magento\SalesRule\Model\RuleFactory $salesRuleFactory + * @param RuleFactory $salesRuleFactory + * @param SimpleActionOptionsProvider|null $simpleActionOptionsProvider */ public function __construct( Store $store, GroupRepositoryInterface $groupRepository, SearchCriteriaBuilder $searchCriteriaBuilder, DataObject $objectConverter, - \Magento\SalesRule\Model\RuleFactory $salesRuleFactory + RuleFactory $salesRuleFactory, + SimpleActionOptionsProvider $simpleActionOptionsProvider = null ) { $this->store = $store; $this->groupRepository = $groupRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->objectConverter = $objectConverter; $this->salesRuleFactory = $salesRuleFactory; + $this->simpleActionOptionsProvider = $simpleActionOptionsProvider ?: + ObjectManager::getInstance()->get(SimpleActionOptionsProvider::class); } /** @@ -71,15 +83,10 @@ public function __construct( * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function getMetadataValues(\Magento\SalesRule\Model\Rule $rule) + public function getMetadataValues(Rule $rule) { $customerGroups = $this->groupRepository->getList($this->searchCriteriaBuilder->create())->getItems(); - $applyOptions = [ - ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION], - ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION], - ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION], - ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION] - ]; + $applyOptions = $this->simpleActionOptionsProvider->toOptionArray(); $couponTypesOptions = []; $couponTypes = $this->salesRuleFactory->create()->getCouponTypes(); diff --git a/app/code/Magento/SalesRule/Model/RuleSearchResult.php b/app/code/Magento/SalesRule/Model/RuleSearchResult.php new file mode 100644 index 0000000000000..834b2b575ec2d --- /dev/null +++ b/app/code/Magento/SalesRule/Model/RuleSearchResult.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\SalesRule\Api\Data\RuleSearchResultInterface; + +/** + * Service Data Object with Sales Rule search results. + * + * @phpcs:ignoreFile + */ +class RuleSearchResult extends SearchResults implements RuleSearchResultInterface +{ + /** + * @inheritdoc + */ + public function setItems(array $items = null) + { + return parent::setItems($items); + } +} diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index f771a4f1e3892..270732c8e0278 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -6,12 +6,18 @@ namespace Magento\SalesRule\Model; use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; use Magento\Framework\App\ObjectManager; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory; +use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory; +use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; +use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; /** * Class RulesApplier + * * @package Magento\SalesRule\Model\Validator */ class RulesApplier @@ -39,29 +45,61 @@ class RulesApplier private $calculatorFactory; /** - * @param \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory + * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory + */ + protected $discountFactory; + + /** + * @var RuleDiscountInterfaceFactory + */ + private $discountInterfaceFactory; + + /** + * @var DiscountDataInterfaceFactory + */ + private $discountDataInterfaceFactory; + + /** + * @var array + */ + private $discountAggregator; + + /** + * RulesApplier constructor. + * @param CalculatorFactory $calculatorFactory * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\SalesRule\Model\Utility $utility + * @param Utility $utility * @param ChildrenValidationLocator|null $childrenValidationLocator + * @param DataFactory|null $discountDataFactory + * @param RuleDiscountInterfaceFactory|null $discountInterfaceFactory + * @param DiscountDataInterfaceFactory|null $discountDataInterfaceFactory */ public function __construct( \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\SalesRule\Model\Utility $utility, - ChildrenValidationLocator $childrenValidationLocator = null + ChildrenValidationLocator $childrenValidationLocator = null, + DataFactory $discountDataFactory = null, + RuleDiscountInterfaceFactory $discountInterfaceFactory = null, + DiscountDataInterfaceFactory $discountDataInterfaceFactory = null ) { $this->calculatorFactory = $calculatorFactory; $this->validatorUtility = $utility; $this->_eventManager = $eventManager; $this->childrenValidationLocator = $childrenValidationLocator ?: ObjectManager::getInstance()->get(ChildrenValidationLocator::class); + $this->discountFactory = $discountDataFactory ?: ObjectManager::getInstance()->get(DataFactory::class); + $this->discountInterfaceFactory = $discountInterfaceFactory + ?: ObjectManager::getInstance()->get(RuleDiscountInterfaceFactory::class); + $this->discountDataInterfaceFactory = $discountDataInterfaceFactory + ?: ObjectManager::getInstance()->get(DiscountDataInterfaceFactory::class); } /** * Apply rules to current order item * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\ResourceModel\Rule\Collection $rules + * @param AbstractItem $item + * @param Collection $rules * @param bool $skipValidation * @param mixed $couponCode * @return array @@ -71,7 +109,8 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) { $address = $item->getAddress(); $appliedRuleIds = []; - /* @var $rule \Magento\SalesRule\Model\Rule */ + $this->discountAggregator = []; + /* @var $rule Rule */ foreach ($rules as $rule) { if (!$this->validatorUtility->canProcessRule($rule, $address)) { continue; @@ -79,7 +118,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) if (!$skipValidation && !$rule->getActions()->validate($item)) { if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) { - continue; + continue; } $childItems = $item->getChildren(); $isContinue = true; @@ -110,7 +149,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) * Add rule discount description label to address object * * @param Address $address - * @param \Magento\SalesRule\Model\Rule $rule + * @param Rule $rule * @return $this */ public function addDiscountDescription($address, $rule) @@ -123,6 +162,10 @@ public function addDiscountDescription($address, $rule) } else { if (strlen($address->getCouponCode())) { $label = $address->getCouponCode(); + + if ($rule->getDescription()) { + $label = $rule->getDescription(); + } } } @@ -136,15 +179,17 @@ public function addDiscountDescription($address, $rule) } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\Rule $rule + * Apply Rule + * + * @param AbstractItem $item + * @param Rule $rule * @param \Magento\Quote\Model\Quote\Address $address * @param mixed $couponCode * @return $this */ protected function applyRule($item, $rule, $address, $couponCode) { - $discountData = $this->getDiscountData($item, $rule); + $discountData = $this->getDiscountData($item, $rule, $address); $this->setDiscountData($discountData, $item); $this->maintainAddressCouponCode($address, $rule, $couponCode); @@ -154,20 +199,23 @@ protected function applyRule($item, $rule, $address, $couponCode) } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * Get discount Data + * + * @param AbstractItem $item * @param \Magento\SalesRule\Model\Rule $rule + * @param \Magento\Quote\Model\Quote\Address $address * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data */ - protected function getDiscountData($item, $rule) + protected function getDiscountData($item, $rule, $address) { $qty = $this->validatorUtility->getItemQty($item, $rule); $discountCalculator = $this->calculatorFactory->create($rule->getSimpleAction()); $qty = $discountCalculator->fixQuantity($qty, $rule); $discountData = $discountCalculator->calculate($rule, $item, $qty); - $this->eventFix($discountData, $item, $rule, $qty); $this->validatorUtility->deltaRoundingFix($discountData, $item); + $this->setDiscountBreakdown($discountData, $item, $rule, $address); /** * We can't use row total here because row total not include tax @@ -180,8 +228,43 @@ protected function getDiscountData($item, $rule) } /** + * Set Discount Breakdown + * * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param \Magento\SalesRule\Model\Rule $rule + * @param \Magento\Quote\Model\Quote\Address $address + * @return $this + */ + private function setDiscountBreakdown($discountData, $item, $rule, $address) + { + if ($discountData->getAmount() > 0 && $item->getExtensionAttributes()) { + $data = [ + 'amount' => $discountData->getAmount(), + 'base_amount' => $discountData->getBaseAmount(), + 'original_amount' => $discountData->getOriginalAmount(), + 'base_original_amount' => $discountData->getBaseOriginalAmount() + ]; + $itemDiscount = $this->discountDataInterfaceFactory->create(['data' => $data]); + $ruleLabel = $rule->getStoreLabel($address->getQuote()->getStore()) ?: __('Discount'); + $data = [ + 'discount' => $itemDiscount, + 'rule' => $ruleLabel, + 'rule_id' => $rule->getId(), + ]; + /** @var \Magento\SalesRule\Model\Data\RuleDiscount $itemDiscount */ + $ruleDiscount = $this->discountInterfaceFactory->create(['data' => $data]); + $this->discountAggregator[] = $ruleDiscount; + $item->getExtensionAttributes()->setDiscounts($this->discountAggregator); + } + return $this; + } + + /** + * Set Discount data + * + * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData + * @param AbstractItem $item * @return $this */ protected function setDiscountData($discountData, $item) @@ -198,7 +281,7 @@ protected function setDiscountData($discountData, $item) * Set coupon code to address if $rule contains validated coupon * * @param Address $address - * @param \Magento\SalesRule\Model\Rule $rule + * @param Rule $rule * @param mixed $couponCode * @return $this */ @@ -208,7 +291,7 @@ public function maintainAddressCouponCode($address, $rule, $couponCode) Rule is a part of rules collection, which includes only rules with 'No Coupon' type or with validated coupon. As a result, if rule uses coupon code(s) ('Specific' or 'Auto' Coupon Type), it always contains validated coupon */ - if ($rule->getCouponType() != \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON) { + if ($rule->getCouponType() != Rule::COUPON_TYPE_NO_COUPON) { $address->setCouponCode($couponCode); } @@ -219,15 +302,15 @@ public function maintainAddressCouponCode($address, $rule, $couponCode) * Fire event to allow overwriting of discount amounts * * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\Rule $rule + * @param AbstractItem $item + * @param Rule $rule * @param float $qty * @return $this */ protected function eventFix( \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData, - \Magento\Quote\Model\Quote\Item\AbstractItem $item, - \Magento\SalesRule\Model\Rule $rule, + AbstractItem $item, + Rule $rule, $qty ) { $quote = $item->getQuote(); @@ -249,11 +332,13 @@ protected function eventFix( } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * Set Applied Rule Ids + * + * @param AbstractItem $item * @param int[] $appliedRuleIds * @return $this */ - public function setAppliedRuleIds(\Magento\Quote\Model\Quote\Item\AbstractItem $item, array $appliedRuleIds) + public function setAppliedRuleIds(AbstractItem $item, array $appliedRuleIds) { $address = $item->getAddress(); $quote = $item->getQuote(); diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml index f9bc44a11cc47..91ecaff7f83a6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -105,7 +105,45 @@ <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="waitForCategoryVisible" after="openChooser"/> <checkOption selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="checkCategoryName" after="waitForCategoryVisible"/> </actionGroup> - + <actionGroup name="AdminCreateCartPriceRuleActionsWithSubtotalActionGroup" extends="AdminCreateCartPriceRuleActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateCartPriceRuleActionGroup. Removes 'fillDiscountAmount'. Adds sub total conditions for free shipping to a Cart Price Rule.</description> + </annotations> + <arguments> + <argument name="ruleName"/> + </arguments> + <remove keyForRemoval="fillDiscountAmount"/> + <!-- Expand the conditions section --> + <grabTextFrom selector="{{AdminCartPriceRulesFormSection.ruleName}}" after="fillRuleName" stepKey="getSubtotalRule"/> + <click selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" stepKey="openConditionsSection" after="selectActionType"/> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1')}}" after="openConditionsSection" stepKey="addFirstCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.conditionSelect}}" userInput="{{ruleName.condition1}}" after="addFirstCondition" stepKey="selectCondition1"/> + <waitForPageLoad after="selectCondition1" stepKey="waitForConditionLoad"/> + <click selector="{{AdminCartPriceRulesFormSection.condition(ruleName.ruleToChange1)}}" after="waitForConditionLoad" stepKey="clickToChooseOption"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.conditionsOperator}}" userInput="{{ruleName.rule1}}" after="clickToChooseOption" stepKey="setOperatorType"/> + <click selector="{{AdminCartPriceRulesFormSection.targetEllipsis}}" after="setOperatorType" stepKey="clickEllipsis"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleFieldByIndex('1--1')}}" userInput="{{ruleName.subtotal}}" after="clickEllipsis" stepKey="fillSubtotalParameter"/> + <click selector="{{AdminCartPriceRulesFormSection.addNewCondition('1')}}" after="fillSubtotalParameter" stepKey="clickOnTheAddNewCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.conditionSelectDropdown('1')}}" userInput="{{ruleName.condition2}}" after="clickOnTheAddNewCondition" stepKey="selectSecondCondition"/> + <waitForPageLoad after="selectSecondCondition" stepKey="waitForConditionLoad2"/> + <click selector="{{AdminCartPriceRulesFormSection.targetEllipsis}}" after="waitForConditionLoad2" stepKey="clickEllipsis2"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleFieldByIndex('1--2')}}" userInput="{{ruleName.shippingMethod}}" after="clickEllipsis2" stepKey="selectShippingMethod"/> + <click selector="{{AdminCartPriceRulesFormSection.applyToShippingAmount}}" after="selectShippingMethod" stepKey="clickApplyToShipping"/> + <click selector="{{AdminCartPriceRulesFormSection.discardSubsequentRules}}" after="clickApplyToShipping" stepKey="clickDiscardSubsequentRules"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.freeShipping}}" userInput="{{ruleName.simple_free_shipping}}" after="clickDiscardSubsequentRules" stepKey="selectForMatchingItemsOnly"/> + </actionGroup> + <actionGroup name="AdminCreateMultiWebsiteCartPriceRuleActionGroup" extends="AdminCreateCartPriceRuleActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateCartPriceRuleActionGroup. Removes 'clickSaveButton' for the next data changing. Assign cart price rule to 2 websites instead of 1.</description> + </annotations> + <arguments> + <argument name="ruleName"/> + </arguments> + <remove keyForRemoval="clickSaveButton"/> + <remove keyForRemoval="seeSuccessMessage"/> + <remove keyForRemoval="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" parameterArray="['FirstWebsite', 'SecondWebsite']" stepKey="selectWebsites" after="fillRuleName"/> + </actionGroup> <actionGroup name="CreateCartPriceRuleSecondWebsiteActionGroup"> <annotations> <description>Goes to the Admin Cart Price Rule grid page. Clicks on Add New Rule. Fills the provided Rule (Name). Selects 'Second Website' from the 'Websites' menu.</description> @@ -128,4 +166,28 @@ <click selector="{{AdminCartPriceRulesFormSection.active}}" stepKey="clickActiveToDisable" after="fillRuleName"/> </actionGroup> + + <actionGroup name="AdminCreateCartPriceRuleWithConditionIsCategoryActionGroup" extends="AdminCreateCartPriceRuleActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateCartPriceRuleActionGroup. Sets the provide Condition (Actions Aggregator/Value, Child Attribute and Action Value) for Actions on the Admin Cart Price Rule creation/edit page.</description> + </annotations> + <arguments> + <argument name="actionsAggregator" type="string" defaultValue="ANY"/> + <argument name="actionsValue" type="string" defaultValue="FALSE"/> + <argument name="childAttribute" type="string" defaultValue="Category"/> + <argument name="actionValue" type="string" defaultValue="2"/> + </arguments> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" after="fillDiscountAmount" stepKey="clickOnActionTab"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('ALL')}}" after="clickOnActionTab" stepKey="clickToChooseFirstRuleConditionValue"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsAggregator}}" userInput="{{actionsAggregator}}" after="clickToChooseFirstRuleConditionValue" stepKey="changeFirstRuleConditionValue"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('TRUE')}}" after="changeFirstRuleConditionValue" stepKey="clickToChooseSecondRuleConditionValue"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsValue}}" userInput="{{actionsValue}}" after="clickToChooseSecondRuleConditionValue" stepKey="changeSecondRuleConditionValue"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" after="changeSecondRuleConditionValue" stepKey="clickConditionDropDownMenu"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="{{childAttribute}}" after="clickConditionDropDownMenu" stepKey="selectConditionAttributeIsCategory"/> + <waitForPageLoad after="selectConditionAttributeIsCategory" stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" after="waitForOperatorOpened" stepKey="clickToChooserIcon"/> + <fillField selector="{{AdminCartPriceRulesFormSection.actionValue}}" userInput="{{actionValue}}" after="clickToChooserIcon" stepKey="choseNeededCategoryFromCategoryGrid"/> + <click selector="{{AdminCartPriceRulesFormSection.applyAction}}" after="choseNeededCategoryFromCategoryGrid" stepKey="applyAction"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml index 17beb4bebc7bd..af9c462bfd42c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml @@ -15,13 +15,16 @@ <arguments> <argument name="ruleName" type="entity"/> </arguments> - + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="goToCartPriceRules"/> <waitForPageLoad stepKey="waitForCartPriceRules"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="resetFilterBeforeDelete"/> + <waitForPageLoad stepKey="waitForCartPriceRulesResetFilter"/> <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName.name}}" stepKey="filterByName"/> <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> <click selector="{{AdminCartPriceRulesFormSection.delete}}" stepKey="clickDeleteButton"/> - <click selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="confirmDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index c1ec728a6cfb9..4a39e1237841d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -89,6 +89,13 @@ <data key="apply">Percent of product price discount</data> <data key="discountAmount">50</data> </entity> + <entity name="SalesRuleWithFullDiscount" type="SalesRule"> + <data key="name" unique="suffix">TestSalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Percent of product price discount</data> + <data key="discountAmount">100</data> + </entity> <entity name="CatPriceRule" type="SalesRule"> <data key="name" unique="suffix">CartPriceRule</data> <data key="websites">Main Website</data> @@ -457,4 +464,13 @@ <requiredEntity type="SalesRuleLabel">SalesRuleLabelDefault</requiredEntity> <requiredEntity type="SalesRuleLabel">SalesRuleLabelStore1</requiredEntity> </entity> + + <entity name="TestSalesRuleWithInvalidData" type="SalesRule"> + <data key="userPerCustomer">one</data> + <data key="userPerCoupon">one</data> + <data key="priority">one</data> + <data key="discountStep">one</data> + <data key="discountAmount">one</data> + <data key="maximumQtyDiscount">one</data> + </entity> </entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index 0755843861247..b164cdde33248 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -19,6 +19,7 @@ <element name="description" type="textarea" selector="//div[@class='admin__field-control']/textarea[@name='description']"/> <element name="active" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='is_active']/../label"/> <element name="websites" type="multiselect" selector="select[name='website_ids']"/> + <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> <element name="customerGroupsOptions" type="multiselect" selector="select[name='customer_group_ids'] option"/> <element name="coupon" type="select" selector="select[name='coupon_type']"/> @@ -97,5 +98,7 @@ <element name="couponQty" type="input" selector="#coupons_qty"/> <element name="generateCouponsButton" type="button" selector="#coupons_generate_button" timeout="30"/> <element name="generatedCouponByIndex" type="text" selector="#couponCodesGrid_table > tbody > tr:nth-child({{var}}) > td.col-code" parameterized="true"/> + <element name="couponGridUsedHeader" type="text" selector="#couponCodesGrid thead th[data-sort='used']"/> + <element name="fieldError" type="text" selector="//input[@name='{{fieldName}}']/following-sibling::label[@class='admin__field-error']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml index 9a74ced2a2c17..89398051fcf67 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml @@ -11,6 +11,7 @@ <element name="rulesDropdown" type="select" selector="select[data-form-part='sales_rule_form'][data-ui-id='newchild-0-select-rule-conditions-1-new-child']"/> <element name="addProductAttributesButton" type="text" selector="#conditions__1--1__children>li>span>a"/> <element name="productAttributesDropdown" type="select" selector="#conditions__1--1__new_child"/> + <element name="firstProductAttributeSelected" type="select" selector="#conditions__1__children .rule-param:nth-of-type(2) a:nth-child(1)"/> <element name="changeCategoriesButton" type="text" selector="#conditions__1--1__children>li>span.rule-param:nth-of-type(2)>a"/> <element name="categoriesChooser" type="text" selector="#conditions__1--1__children>li>span.rule-param:nth-of-type(2)>span>label>a"/> <element name="treeRoot" type="text" selector=".x-tree-root-ct.x-tree-lines"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml index 92d221de9e157..02078ff15ecc2 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule of type Buy X get Y free --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 03dffe9f448ea..d719bb90efd59 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule --> @@ -62,6 +62,9 @@ <waitForPageLoad stepKey="waitFormToReload1"/> <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection2"/> + <!-- Assert coupon codes grid header is correct --> + <see selector="{{AdminCartPriceRulesFormSection.couponGridUsedHeader}}" userInput="Used" stepKey="seeCorrectUsedHeader"/> + <!-- Grab a coupon code and hold on to it for later --> <grabTextFrom selector="{{AdminCartPriceRulesFormSection.generatedCouponByIndex('1')}}" stepKey="grabCouponCode"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml index 08a08275ee07a..1681d910ccdb0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule for $10 Fixed amount discount --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index a39530f7607e4..69918bda8c426 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule for Fixed amount discount for whole cart --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml new file mode 100644 index 0000000000000..620112e323ff5 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml @@ -0,0 +1,53 @@ +<?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="AdminCreateInvalidRuleTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule with invalid data"/> + <title value="Admin can not create rule with invalid data"/> + <description value="Admin can not create rule with invalid data"/> + <severity value="MAJOR"/> + <group value="SalesRule"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Buy X get Y free (discount amount is Y)" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{TestSalesRuleWithInvalidData.discountAmount}}" stepKey="fillDiscountAmount"/> + <fillField selector="{{AdminCartPriceRulesFormSection.maximumQtyDiscount}}" userInput="{{TestSalesRuleWithInvalidData.maximumQtyDiscount}}" stepKey="fillDiscountQty"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountStep}}" userInput="{{TestSalesRuleWithInvalidData.discountStep}}" stepKey="fillDiscountStep"/> + + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCustomer}}" userInput="{{TestSalesRuleWithInvalidData.userPerCustomer}}" stepKey="fillUsePerCustomer"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="{{TestSalesRuleWithInvalidData.userPerCoupon}}" stepKey="fillUsePerCoupon"/> + <fillField selector="{{AdminCartPriceRulesFormSection.priority}}" userInput="{{TestSalesRuleWithInvalidData.priority}}" stepKey="fillPriority"/> + + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + + <see selector="{{AdminNewCatalogPriceRule.fieldError('uses_per_coupon')}}" userInput="Please enter a valid number in this field." stepKey="seePerCouponError"/> + <see selector="{{AdminNewCatalogPriceRule.fieldError('uses_per_customer')}}" userInput="Please enter a valid number in this field." stepKey="seePerCustomerError"/> + <see selector="{{AdminNewCatalogPriceRule.fieldError('sort_order')}}" userInput="Please enter a valid number in this field." stepKey="seePriorityError"/> + <see selector="{{AdminNewCatalogPriceRule.fieldError('discount_amount')}}" userInput="Please enter a valid number in this field." stepKey="seeDiscountAmountError"/> + <see selector="{{AdminNewCatalogPriceRule.fieldError('discount_qty')}}" userInput="Please enter a valid number in this field." stepKey="seeMaximumQtyError"/> + <see selector="{{AdminNewCatalogPriceRule.fieldError('discount_step')}}" userInput="Please enter a valid number in this field." stepKey="seeDiscountStepError"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml index 1f7d849ac02b0..898e5a07304b6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule for 50 percent of product price --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml index d106c086a6065..9a71210aac1c6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml @@ -16,9 +16,6 @@ <severity value="CRITICAL"/> <group value="salesRule"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-17175"/> - </skip> </annotations> <before> @@ -60,6 +57,8 @@ <actionGroup ref="AdminCreateCartPriceRuleLabelsSectionActionGroup" stepKey="createActiveCartPriceRuleLabelsSection"> <argument name="rule" value="ActiveSalesRuleWithComplexConditions"/> </actionGroup> + <generateDate date="+1 minute" format="m/d/Y" stepKey="generateStartDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$generateStartDate}" stepKey="fillStartDate"/> <actionGroup ref="AssertCartPriceRuleSuccessSaveMessageActionGroup" stepKey="assertVerifyCartPriceRuleSuccessSaveMessage"/> </before> <after> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 0d365dc089e43..e4e9a62780948 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -38,6 +38,45 @@ <argument name="total" value="447.00"/> </actionGroup> + <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> + </actionGroup> + <comment userInput="End of using coupon code" stepKey="endOfUsingCouponCode" after="cartAssertCartAfterCancelCoupon" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <before> + <createData entity="ApiSalesRule" stepKey="createSalesRule"/> + <createData entity="ApiSalesRuleCoupon" stepKey="createSalesRuleCoupon"> + <requiredEntity createDataKey="createSalesRule"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + </after> + + <!-- Step 5: User uses coupon codes --> + <comment userInput="Start of using coupon code" stepKey="startOfUsingCouponCode" after="endOfComparingProducts" /> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="couponOpenCart" after="startOfUsingCouponCode"/> + + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="couponApplyCoupon" after="couponOpenCart"> + <argument name="coupon" value="$$createSalesRuleCoupon$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCheckCouponAppliedActionGroup" stepKey="couponCheckAppliedDiscount" after="couponApplyCoupon"> + <argument name="rule" value="$$createSalesRule$$"/> + <argument name="discount" value="48.00"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="couponCheckCartWithDiscount" after="couponCheckAppliedDiscount"> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="447.00"/> + </actionGroup> + <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> <argument name="subtotal" value="480.00"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml new file mode 100644 index 0000000000000..9e95e39e4791e --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml @@ -0,0 +1,100 @@ +<?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="StorefrontCartRuleCouponForFreeShippingTest"> + <annotations> + <stories value="Create Sales Rule"/> + <title value="Create Cart Price Rule for Free Shipping And Verify Coupon Code will shown in Order's totals"/> + <description value="Test that Coupon Code of Cart Price Rule without discount for Product price but with Free shipping will shown in Order's totals"/> + <testCaseId value="MC-21923"/> + <useCaseId value="MC-20387"/> + <severity value="MAJOR"/> + <group value="SalesRule"/> + </annotations> + + <before> + <!-- Create Simple Product --> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> + <!-- Create Cart Price Rule without discount but with free shipping --> + <createData entity="ApiSalesRule" stepKey="createCartPriceRule"> + <field key="simple_free_shipping">1</field> + <field key="discount_amount">0</field> + </createData> + <!-- Create Coupon code for the Cart Price Rule --> + <createData entity="ApiSalesRuleCoupon" stepKey="createCartPriceRuleCoupon"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + <!-- Create Customer with filled Shipping & Billing Address --> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + </before> + + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromStorefront"/> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteSalesRule"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logoutFromBackend"/> + </after> + + <!-- Login with created Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add Simple Product to Cart --> + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout"/> + + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStep" stepKey="goToCheckoutReview"/> + + <!-- Apply Discount Coupon to the Order --> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> + <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> + </actionGroup> + + <!-- Assert Coupon Code will shown in Shipping total --> + <actionGroup ref="AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup" stepKey="assertCouponCodeInShippingLabel"> + <argument name="labelDescription" value="$createCartPriceRuleCoupon.code$"/> + </actionGroup> + + <!-- Select payment solution --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="clickCheckMoneyOrderPayment"/> + + <!-- Place Order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + + <!-- Go To Order View --> + <click selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="goToViewOrder"/> + + <!-- Assert Coupon Code will shown in Shipping total description in Order View page --> + <actionGroup ref="AssertStorefrontShippingDescriptionInOrderViewActionGroup" stepKey="assertCouponCodeInShippingTotalDescription"> + <argument name="description" value="$createCartPriceRuleCoupon.code$"/> + </actionGroup> + + <!-- Keep Order Id --> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + + <!-- Login to admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Go to created Order --> + <amOnPage url="{{AdminOrderPage.url({$grabOrderId})}}" stepKey="goToAdminViewOrder"/> + <waitForPageLoad stepKey="waitForOrderPage"/> + + <!-- Assert Coupon Code will shown in Shipping total description --> + <actionGroup ref="AssertAdminShippingDescriptionInOrderViewActionGroup" stepKey="seeCouponInShippingDescription"> + <argument name="description" value="$createCartPriceRuleCoupon.code$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml new file mode 100644 index 0000000000000..903e79b2328bc --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml @@ -0,0 +1,125 @@ +<?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="StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Cart total with full discount"/> + <title value="Cart Total value when 100% discount applied through Cart Rule"/> + <description value="Cart Total value when 100% discount applied through Cart Rule"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-19524"/> + <useCaseId value="MC-17869"/> + <group value="SalesRule"/> + </annotations> + <before> + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Set configurations --> + <magentoCLI command="config:set carriers/tablerate/active 1" stepKey="setShippingMethodEnabled"/> + <magentoCLI command="config:set carriers/tablerate/condition_name package_value" stepKey="setShippingMethodConditionName"/> + <magentoCLI command="config:set tax/calculation/price_includes_tax 1" stepKey="setCatalogPrice"/> + <magentoCLI command="config:set tax/calculation/shipping_includes_tax 1" stepKey="setSippingPrice"/> + <magentoCLI command="config:set tax/calculation/cross_border_trade_enabled 0" stepKey="setCrossBorderTrade"/> + <magentoCLI command="config:set tax/calculation/discount_tax 1" stepKey="setDiscount"/> + <magentoCLI command="config:set tax/cart_display/price 2" stepKey="setPrice"/> + <magentoCLI command="config:set tax/cart_display/subtotal 2" stepKey="setSubtotal"/> + <magentoCLI command="config:set carriers/freeshipping/active 1" stepKey="setFreeShipping"/> + <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <!-- Go to tax rule page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> + <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> + <!-- Add tax rule with 20% tax rate --> + <actionGroup ref="addNewTaxRateNoZip" stepKey="addNYTaxRate"> + <argument name="taxCode" value="SimpleTaxNYRate"/> + </actionGroup> + <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <!-- Create cart price rule --> + <actionGroup ref="AdminCreateCartPriceRuleActionGroup" stepKey="createCartPriceRule"> + <argument name="ruleName" value="SalesRuleWithFullDiscount"/> + </actionGroup> + <!-- Create 3 simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProductFirst"> + <field key="price">5.10</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createSimpleProductSecond"> + <field key="price">5.10</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createSimpleProductThird"> + <field key="price">5.50</field> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Removed created Data --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> + <argument name="name" value="SampleRule"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + <!-- Delete the tax rate that were created --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> + <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> + <argument name="name" value="{{SimpleTaxNYRate.state}}-{{SimpleTaxNYRate.rate}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{SalesRuleWithFullDiscount.name}}"/> + </actionGroup> + <!-- Delete products --> + <deleteData createDataKey="createSimpleProductFirst" stepKey="deleteSimpleProductFirst"/> + <deleteData createDataKey="createSimpleProductSecond" stepKey="deleteSimpleProductSecond"/> + <deleteData createDataKey="createSimpleProductThird" stepKey="deleteSimpleProductThird"/> + <!-- Unset configuration --> + <magentoCLI command="config:set carriers/tablerate/active 0" stepKey="unsetShippingMethodEnabled"/> + <magentoCLI command="config:set tax/calculation/price_includes_tax 0" stepKey="unsetCatalogPrice"/> + <magentoCLI command="config:set tax/calculation/shipping_includes_tax 0" stepKey="unsetSippingPrice"/> + <magentoCLI command="config:set tax/calculation/cross_border_trade_enabled 1" stepKey="unsetCrossBorderTrade"/> + <magentoCLI command="config:set tax/calculation/discount_tax 0" stepKey="unsetDiscount"/> + <magentoCLI command="config:set tax/cart_display/price 1" stepKey="unsetPrice"/> + <magentoCLI command="config:set tax/cart_display/subtotal 1" stepKey="unsetSubtotal"/> + <magentoCLI command="config:set carriers/freeshipping/active 0" stepKey="unsetFreeShipping"/> + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Add testing products to the cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProductFirst.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="2" stepKey="setQuantity"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addProductToCard"> + <argument name="productName" value="$$createSimpleProductFirst.name$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad"/> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProductSecond.custom_attributes[url_key]$$)}}" stepKey="goToSecondProductPage"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="2" stepKey="setQuantityForTheSecondProduct"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addSecondProductToCard"> + <argument name="productName" value="$$createSimpleProductSecond.name$$"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProductThird.custom_attributes[url_key]$$)}}" stepKey="goToThirdProductPage"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="2" stepKey="setQuantityForTheThirdProduct"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addThirdProductToCard"> + <argument name="productName" value="$$createSimpleProductThird.name$$"/> + </actionGroup> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="6" stepKey="seeCartQuantity"/> + <!-- Go to the shopping cart page --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.orderTotal}}" stepKey="waitForOrderTotalVisible"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectCountry"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForOrderTotalUpdate"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$29.00" stepKey="seeDiscountAmount"/> + <see selector="{{CheckoutCartSummarySection.subTotal}}" userInput="$29.00" stepKey="seeSubTotal"/> + <see selector="{{CheckoutCartSummarySection.orderTotal}}" userInput="0.00" stepKey="seeOrderTotal"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml new file mode 100644 index 0000000000000..8900d838fb825 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml @@ -0,0 +1,117 @@ +<?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="StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Category rules should apply to grouped product with invisible individual products"/> + <description value="Category rules should apply to grouped product with invisible individual products"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13608"/> + <group value="SalesRule"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategoryOne"/> + <createData entity="ApiSimpleProduct" stepKey="createFirstSimpleProduct"> + <field key ="price">100</field> + <field key="visibility">1</field> + <requiredEntity createDataKey="createCategoryOne"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSecondSimpleProduct"> + <field key ="price">200</field> + <field key="visibility">1</field> + <requiredEntity createDataKey="createCategoryOne"/> + </createData> + <createData entity="ApiCategory" stepKey="createCategoryTwo"/> + <createData entity="ApiSimpleProduct" stepKey="createThirdSimpleProduct"> + <field key ="price">300</field> + <field key="visibility">1</field> + <requiredEntity createDataKey="createCategoryTwo"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createFourthSimpleProduct"> + <field key ="price">400</field> + <field key="visibility">1</field> + <requiredEntity createDataKey="createCategoryTwo"/> + </createData> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategoryOne"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </updateData> + <createData entity="ApiGroupedProduct2" stepKey="createSecondGroupedProduct"> + <requiredEntity createDataKey="createCategoryTwo"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addThirdProduct"> + <requiredEntity createDataKey="createSecondGroupedProduct"/> + <requiredEntity createDataKey="createThirdSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addThirdProduct" stepKey="addFourthProduct"> + <requiredEntity createDataKey="createSecondGroupedProduct"/> + <requiredEntity createDataKey="createFourthSimpleProduct"/> + </updateData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createThirdSimpleProduct" stepKey="deleteThirdSimpleProduct"/> + <deleteData createDataKey="createFourthSimpleProduct" stepKey="deleteFourthSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + <deleteData createDataKey="createSecondGroupedProduct" stepKey="deleteSecondGroupedProduct"/> + <deleteData createDataKey="createCategoryOne" stepKey="deleteCategoryOne"/> + <deleteData createDataKey="createCategoryTwo" stepKey="deleteCategoryTwo"/> + <actionGroup ref="AdminDeleteCartPriceRuleActionGroup" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="TestSalesRule"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Start to create new cart price rule via Category conditions --> + <actionGroup ref="AdminCreateCartPriceRuleWithConditionIsCategoryActionGroup" stepKey="createCartPriceRuleWithCondition"> + <argument name="ruleName" value="TestSalesRule"/> + <argument name="actionValue" value="$createCategoryTwo.id$"/> + </actionGroup> + <!-- Add SecondGroupedProduct to the cart --> + <actionGroup ref="StorefrontAddGroupedProductWithTwoLinksToCartActionGroup" stepKey="addSecondGroupedProductToCart"> + <argument name="product" value="$createSecondGroupedProduct$"/> + <argument name="linkedProduct1Name" value="$createThirdSimpleProduct.name$"/> + <argument name="linkedProduct2Name" value="$createFourthSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openTheCartWithSecondGroupedProduct"/> + <!-- Discount amount is not applied --> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="checkDiscountIsNotApplied"> + <argument name="subtotal" value="$700.00"/> + <argument name="shipping" value="$10.00"/> + <argument name="total" value="$710.00"/> + </actionGroup> + <!-- Discount is absent in cart subtotal --> + <dontSeeElement selector="{{CheckoutCartSummarySection.discountLabel}}" stepKey="discountIsNotApplied"/> + <!-- Add FirstGroupedProduct to the cart --> + <actionGroup ref="StorefrontAddGroupedProductWithTwoLinksToCartActionGroup" stepKey="addFirsGroupedProductToCart"> + <argument name="product" value="$createGroupedProduct$"/> + <argument name="linkedProduct1Name" value="$createFirstSimpleProduct.name$"/> + <argument name="linkedProduct2Name" value="$createSecondSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openTheCartWithFirstAndSecondGroupedProducts"/> + <!-- Discount amount is applied for product from first category only --> + <actionGroup ref="StorefrontCheckCartTotalWithDiscountCategoryActionGroup" stepKey="checkDiscountIsApplied"> + <argument name="subtotal" value="$1,000.00"/> + <argument name="shipping" value="$20.00"/> + <argument name="discount" value="150.00"/> + <argument name="total" value="$870.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php new file mode 100644 index 0000000000000..b5b6d047c3af2 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php @@ -0,0 +1,228 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Test\Unit\Model\Quote\Address\Total; + +use Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount; +use Magento\SalesRule\Model\Validator; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Api\Data\ShippingInterface; +use Magento\Quote\Model\Quote\Address\Total; + +/** + * Class \Magento\SalesRule\Test\Unit\Model\Quote\Address\Total\ShippingDiscountTest + */ +class ShippingDiscountTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject | Validator + */ + protected $validatorMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | Quote + */ + private $quoteMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | Total + */ + private $totalMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | Address + */ + private $addressMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | ShippingAssignmentInterface + */ + private $shippingAssignmentMock; + + /** + * @var ShippingDiscount + */ + private $discount; + + protected function setUp() + { + $this->validatorMock = $this->getMockBuilder(\Magento\SalesRule\Model\Validator::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'reset', + 'processShippingAmount', + '__wakeup', + ] + ) + ->getMock(); + $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $this->totalMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address\Total::class, + [ + 'getDiscountAmount', + 'getDiscountDescription', + 'addTotalAmount', + 'addBaseTotalAmount', + 'setShippingDiscountAmount', + 'setBaseShippingDiscountAmount', + 'getSubtotal', + 'setSubtotalWithDiscount', + 'setBaseSubtotalWithDiscount', + 'getBaseSubtotal', + 'getBaseDiscountAmount', + 'setDiscountDescription' + ] + ); + + $this->addressMock = $this->createPartialMock( + Address::class, + [ + 'getQuote', + 'getShippingAmount', + 'getShippingDiscountAmount', + 'getBaseShippingDiscountAmount', + 'setShippingDiscountAmount', + 'setBaseShippingDiscountAmount', + 'getDiscountDescription', + 'setDiscountAmount', + 'setBaseDiscountAmount', + '__wakeup' + ] + ); + + $shipping = $this->createMock(ShippingInterface::class); + $shipping->expects($this->any())->method('getAddress')->willReturn($this->addressMock); + $this->shippingAssignmentMock = $this->createMock(ShippingAssignmentInterface::class); + $this->shippingAssignmentMock->expects($this->any())->method('getShipping')->willReturn($shipping); + + $this->discount = new ShippingDiscount( + $this->validatorMock + ); + } + + /** + * Test collect with the quote has no shipping amount discount + */ + public function testCollectNoShippingAmount() + { + $itemNoDiscount = $this->createMock(\Magento\Quote\Model\Quote\Item::class); + + $this->addressMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); + + $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(0); + + $this->shippingAssignmentMock->expects($this->any())->method('getItems') + ->willReturn([$itemNoDiscount]); + + $this->addressMock->expects($this->once())->method('setShippingDiscountAmount') + ->with(0) + ->willReturnSelf(); + $this->addressMock->expects($this->once())->method('setBaseShippingDiscountAmount') + ->with(0) + ->willReturnSelf(); + + /* Assert Collect function */ + $this->assertInstanceOf( + ShippingDiscount::class, + $this->discount->collect($this->quoteMock, $this->shippingAssignmentMock, $this->totalMock) + ); + } + + /** + * Test collect with the quote has shipping amount discount + */ + public function testCollectWithShippingAmountDiscount() + { + $shippingAmount = 100; + $shippingDiscountAmount = 50; + $baseShippingDiscountAmount = 50; + $discountDescription = 'Discount $50'; + $subTotal = 200; + $discountAmount = -100; + $baseSubTotal = 200; + $baseDiscountAmount = -100; + + $itemNoDiscount = $this->createMock(\Magento\Quote\Model\Quote\Item::class); + + $this->addressMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); + + $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn($shippingAmount); + + $this->addressMock->expects($this->any())->method('getShippingDiscountAmount') + ->willReturn($shippingDiscountAmount); + $this->addressMock->expects($this->any())->method('getBaseShippingDiscountAmount') + ->willReturn($baseShippingDiscountAmount); + + $this->addressMock->expects($this->any())->method('getDiscountDescription') + ->willReturn($discountDescription); + + $this->shippingAssignmentMock->expects($this->any())->method('getItems') + ->willReturn([$itemNoDiscount]); + + $this->totalMock->expects($this->once())->method('addTotalAmount') + ->with('discount', -$shippingDiscountAmount)->willReturnSelf(); + $this->totalMock->expects($this->once())->method('addBaseTotalAmount') + ->with('discount', -$baseShippingDiscountAmount)->willReturnSelf(); + + $this->totalMock->expects($this->once())->method('setShippingDiscountAmount') + ->with($shippingDiscountAmount)->willReturnSelf(); + $this->totalMock->expects($this->once())->method('setBaseShippingDiscountAmount') + ->with($baseShippingDiscountAmount)->willReturnSelf(); + + $this->totalMock->expects($this->any())->method('getSubtotal') + ->willReturn($subTotal); + $this->totalMock->expects($this->any())->method('getDiscountAmount') + ->willReturn($discountAmount); + + $this->totalMock->expects($this->any())->method('getBaseSubtotal') + ->willReturn($baseSubTotal); + $this->totalMock->expects($this->any())->method('getBaseDiscountAmount') + ->willReturn($baseDiscountAmount); + + $this->totalMock->expects($this->once())->method('setDiscountDescription') + ->with($discountDescription)->willReturnSelf(); + + $this->totalMock->expects($this->once())->method('setSubtotalWithDiscount') + ->with(100)->willReturnSelf(); + $this->totalMock->expects($this->once())->method('setBaseSubtotalWithDiscount') + ->with(100)->willReturnSelf(); + + $this->addressMock->expects($this->once())->method('setDiscountAmount') + ->with($discountAmount)->willReturnSelf(); + + $this->addressMock->expects($this->once())->method('setBaseDiscountAmount') + ->with($baseDiscountAmount)->willReturnSelf(); + + /* Assert Collect function */ + $this->assertInstanceOf( + ShippingDiscount::class, + $this->discount->collect($this->quoteMock, $this->shippingAssignmentMock, $this->totalMock) + ); + } + + /** + * Test fetch function with discount = 100 + */ + public function testFetch() + { + $discountAmount = 100; + $discountDescription = 100; + $expectedResult = [ + 'code' => 'discount', + 'value' => 100, + 'title' => __('Discount (%1)', $discountDescription) + ]; + $this->totalMock->expects($this->once())->method('getDiscountAmount') + ->willReturn($discountAmount); + $this->totalMock->expects($this->once())->method('getDiscountDescription') + ->willReturn($discountDescription); + $this->assertEquals($expectedResult, $this->discount->fetch($this->quoteMock, $this->totalMock)); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index 090dbd7fe5d6d..72355625318c5 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -47,6 +47,11 @@ class DiscountTest extends \PHPUnit\Framework\TestCase */ protected $addressMock; + /** + * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $discountFactory; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -72,16 +77,35 @@ protected function setUp() $priceCurrencyMock = $this->createMock(\Magento\Framework\Pricing\PriceCurrencyInterface::class); $priceCurrencyMock->expects($this->any()) ->method('round') - ->will($this->returnCallback( - function ($argument) { - return round($argument, 2); - } - )); + ->will( + $this->returnCallback( + function ($argument) { + return round($argument, 2); + } + ) + ); $this->addressMock = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['getQuote', 'getAllItems', 'getShippingAmount', '__wakeup', 'getCustomAttributesCodes'] + [ + 'getQuote', + 'getAllItems', + 'getShippingAmount', + '__wakeup', + 'getCustomAttributesCodes', + 'getExtensionAttributes' + ] ); + $addressExtension = $this->getMockBuilder( + \Magento\Framework\Api\ExtensionAttributesInterface::class + )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + $addressExtension->method('getDiscounts')->willReturn([]); + $addressExtension->expects($this->any()) + ->method('setDiscounts') + ->willReturn([]); + $this->addressMock->expects( + $this->any() + )->method('getExtensionAttributes')->will($this->returnValue($addressExtension)); $this->addressMock->expects($this->any()) ->method('getCustomAttributesCodes') ->willReturn([]); @@ -90,6 +114,10 @@ function ($argument) { $shipping->expects($this->any())->method('getAddress')->willReturn($this->addressMock); $this->shippingAssignmentMock = $this->createMock(\Magento\Quote\Api\Data\ShippingAssignmentInterface::class); $this->shippingAssignmentMock->expects($this->any())->method('getShipping')->willReturn($shipping); + $this->discountFactory = $this->createPartialMock( + \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory::class, + ['create'] + ); /** @var \Magento\SalesRule\Model\Quote\Discount $discount */ $this->discount = $this->objectManager->getObject( @@ -101,14 +129,38 @@ function ($argument) { 'priceCurrency' => $priceCurrencyMock, ] ); + $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class) + ->setConstructorArgs( + [ + 'amount' => 0, + 'baseAmount' => 0, + 'originalAmount' => 0, + 'baseOriginalAmount' => 0 + ] + ) + ->getMock(); + $this->discountFactory->expects($this->any()) + ->method('create') + ->with($this->anything()) + ->will($this->returnValue($discountData)); } public function testCollectItemNoDiscount() { $itemNoDiscount = $this->createPartialMock( \Magento\Quote\Model\Quote\Item::class, - ['getNoDiscount', '__wakeup'] + ['getNoDiscount', '__wakeup', 'getExtensionAttributes'] ); + $itemExtension = $this->getMockBuilder( + \Magento\Framework\Api\ExtensionAttributesInterface::class + )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + $itemExtension->method('getDiscounts')->willReturn([]); + $itemExtension->expects($this->any()) + ->method('setDiscounts') + ->willReturn([]); + $itemNoDiscount->expects( + $this->any() + )->method('getExtensionAttributes')->will($this->returnValue($itemExtension)); $itemNoDiscount->expects($this->once())->method('getNoDiscount')->willReturn(true); $this->validatorMock->expects($this->once())->method('sortItemsByPriority') ->with([$itemNoDiscount], $this->addressMock) @@ -178,10 +230,21 @@ public function testCollectItemHasChildren($childItemData, $parentData, $expecte 'getHasChildren', 'isChildrenCalculated', 'getChildren', + 'getExtensionAttributes', '__wakeup', ] ) ->getMock(); + $itemExtension = $this->getMockBuilder( + \Magento\Framework\Api\ExtensionAttributesInterface::class + )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + $itemExtension->method('getDiscounts')->willReturn([]); + $itemExtension->expects($this->any()) + ->method('setDiscounts') + ->willReturn([]); + $itemWithChildren->expects( + $this->any() + )->method('getExtensionAttributes')->will($this->returnValue($itemExtension)); $itemWithChildren->expects($this->once())->method('getNoDiscount')->willReturn(false); $itemWithChildren->expects($this->once())->method('getParentItem')->willReturn(false); $itemWithChildren->expects($this->once())->method('getHasChildren')->willReturn(true); @@ -310,10 +373,21 @@ public function testCollectItemHasNoChildren() 'getHasChildren', 'isChildrenCalculated', 'getChildren', + 'getExtensionAttributes', '__wakeup', ] ) ->getMock(); + $itemExtension = $this->getMockBuilder( + \Magento\Framework\Api\ExtensionAttributesInterface::class + )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + $itemExtension->method('getDiscounts')->willReturn([]); + $itemExtension->expects($this->any()) + ->method('setDiscounts') + ->willReturn([]); + $itemWithChildren->expects( + $this->any() + )->method('getExtensionAttributes')->will($this->returnValue($itemExtension)); $itemWithChildren->expects($this->once())->method('getNoDiscount')->willReturn(false); $itemWithChildren->expects($this->once())->method('getParentItem')->willReturn(false); $itemWithChildren->expects($this->once())->method('getHasChildren')->willReturn(false); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php new file mode 100644 index 0000000000000..f1653dd043b50 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SalesRule\Test\Unit\Model\Rule\Action; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @covers Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider + */ +class SimpleActionOptionsProviderTest extends TestCase +{ + /** + * @var SimpleActionOptionsProvider|MockObject + */ + protected $model; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->model = $objectManager->getObject(SimpleActionOptionsProvider::class); + } + + public function testToOptionArray() + { + $expected = [ + ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION], + ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION], + ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION], + ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION] + ]; + + $this->assertEquals($expected, $this->model->toOptionArray()); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php index 8ca6b20db3b5a..da358372e0895 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -247,10 +247,10 @@ public function testValidateCategoriesIgnoresVisibility(): void * @param boolean $isValid * @param string $conditionValue * @param string $operator - * @param double $productPrice + * @param string $productPrice * @dataProvider localisationProvider */ - public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator = '>=', $productPrice = 2000.00) + public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator = '>=', $productPrice = '2000.00') { $attr = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php index 0864b4a5e1480..d63ba150f4822 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php @@ -5,52 +5,72 @@ */ namespace Magento\SalesRule\Test\Unit\Model\Rule\Metadata; +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\Data\GroupSearchResultsInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Convert\DataObject; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider; +use Magento\SalesRule\Model\Rule\Metadata\ValueProvider; +use Magento\SalesRule\Model\RuleFactory; +use Magento\Store\Model\System\Store; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; /** * @covers Magento\SalesRule\Model\Rule\Metadata\ValueProvider */ -class ValueProviderTest extends \PHPUnit\Framework\TestCase +class ValueProviderTest extends TestCase { /** - * @var \Magento\SalesRule\Model\Rule\Metadata\ValueProvider + * @var ValueProvider */ protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Store|MockObject */ protected $storeMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var GroupRepositoryInterface|MockObject */ protected $groupRepositoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var SearchCriteriaBuilder|MockObject */ protected $searchCriteriaBuilderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var DataObject|MockObject */ protected $dataObjectMock; /** - * @var \Magento\SalesRule\Model\RuleFactory|\PHPUnit_Framework_MockObject_MockObject + * @var RuleFactory|MockObject */ protected $ruleFactoryMock; + /** + * @var SimpleActionOptionsProvider|MockObject + */ + private $simpleActionOptionsProviderMock; + protected function setUp() { - $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class); - $this->storeMock = $this->createMock(\Magento\Store\Model\System\Store::class); - $this->groupRepositoryMock = $this->createMock(\Magento\Customer\Api\GroupRepositoryInterface::class); - $this->dataObjectMock = $this->createMock(\Magento\Framework\Convert\DataObject::class); - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); - $groupSearchResultsMock = $this->createMock(\Magento\Customer\Api\Data\GroupSearchResultsInterface::class); - $groupsMock = $this->createMock(\Magento\Customer\Api\Data\GroupInterface::class); + $expectedData = include __DIR__ . '/_files/MetaData.php'; + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $this->storeMock = $this->createMock(Store::class); + $this->groupRepositoryMock = $this->createMock(GroupRepositoryInterface::class); + $this->dataObjectMock = $this->createMock(DataObject::class); + $this->simpleActionOptionsProviderMock = $this->createMock(SimpleActionOptionsProvider::class); + $searchCriteriaMock = $this->createMock(SearchCriteriaInterface::class); + $groupSearchResultsMock = $this->createMock(GroupSearchResultsInterface::class); + $groupsMock = $this->createMock(GroupInterface::class); $this->searchCriteriaBuilderMock->expects($this->once())->method('create')->willReturn($searchCriteriaMock); $this->groupRepositoryMock->expects($this->once())->method('getList')->with($searchCriteriaMock) @@ -59,15 +79,19 @@ protected function setUp() $this->storeMock->expects($this->once())->method('getWebsiteValuesForForm')->willReturn([]); $this->dataObjectMock->expects($this->once())->method('toOptionArray')->with([$groupsMock], 'id', 'code') ->willReturn([]); - $this->ruleFactoryMock = $this->createPartialMock(\Magento\SalesRule\Model\RuleFactory::class, ['create']); + $this->ruleFactoryMock = $this->createPartialMock(RuleFactory::class, ['create']); + $this->simpleActionOptionsProviderMock->method('toOptionArray')->willReturn( + $expectedData['actions']['children']['simple_action']['arguments']['data']['config']['options'] + ); $this->model = (new ObjectManager($this))->getObject( - \Magento\SalesRule\Model\Rule\Metadata\ValueProvider::class, + ValueProvider::class, [ 'store' => $this->storeMock, 'groupRepository' => $this->groupRepositoryMock, 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, 'objectConverter' => $this->dataObjectMock, 'salesRuleFactory' => $this->ruleFactoryMock, + 'simpleActionOptionsProvider' => $this->simpleActionOptionsProviderMock ] ); } @@ -76,8 +100,8 @@ public function testGetMetadataValues() { $expectedData = include __DIR__ . '/_files/MetaData.php'; - /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleMock */ - $ruleMock = $this->createMock(\Magento\SalesRule\Model\Rule::class); + /** @var Rule|MockObject $ruleMock */ + $ruleMock = $this->createMock(Rule::class); $this->ruleFactoryMock->expects($this->once()) ->method('create') ->willReturn($ruleMock); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 217a8dba273c4..4260e6b415091 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -6,55 +6,81 @@ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Framework\Event\Manager; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Rule\Model\Action\Collection; +use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory; +use Magento\SalesRule\Model\Rule\Action\Discount\Data; +use Magento\SalesRule\Model\Rule\Action\Discount\DiscountInterface; +use Magento\SalesRule\Model\RulesApplier; +use Magento\SalesRule\Model\Utility; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class RulesApplierTest extends \PHPUnit\Framework\TestCase +class RulesApplierTest extends TestCase { /** - * @var \Magento\SalesRule\Model\RulesApplier + * @var RulesApplier */ protected $rulesApplier; /** - * @var \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CalculatorFactory|PHPUnit_Framework_MockObject_MockObject */ protected $calculatorFactory; /** - * @var \Magento\Framework\Event\Manager|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $discountFactory; + + /** + * @var Manager|PHPUnit_Framework_MockObject_MockObject */ protected $eventManager; /** - * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject + * @var Utility|PHPUnit_Framework_MockObject_MockObject */ protected $validatorUtility; /** - * @var \Magento\SalesRule\Model\Quote\ChildrenValidationLocator|\PHPUnit_Framework_MockObject_MockObject + * @var ChildrenValidationLocator|PHPUnit_Framework_MockObject_MockObject */ protected $childrenValidationLocator; protected function setUp() { $this->calculatorFactory = $this->createMock( - \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory::class + CalculatorFactory::class + ); + $this->discountFactory = $this->createPartialMock( + \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory::class, + ['create'] ); $this->eventManager = $this->createPartialMock(\Magento\Framework\Event\Manager::class, ['dispatch']); $this->validatorUtility = $this->createPartialMock( - \Magento\SalesRule\Model\Utility::class, + Utility::class, ['canProcessRule', 'minFix', 'deltaRoundingFix', 'getItemQty'] ); $this->childrenValidationLocator = $this->createPartialMock( - \Magento\SalesRule\Model\Quote\ChildrenValidationLocator::class, + ChildrenValidationLocator::class, ['isChildrenValidationRequired'] ); - $this->rulesApplier = new \Magento\SalesRule\Model\RulesApplier( + $this->rulesApplier = new RulesApplier( $this->calculatorFactory, $this->eventManager, $this->validatorUtility, - $this->childrenValidationLocator + $this->childrenValidationLocator, + $this->discountFactory ); } @@ -73,21 +99,36 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $ruleId = 1; $appliedRuleIds = [$ruleId => $ruleId]; - + $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class) + ->setConstructorArgs( + [ + 'amount' => 0, + 'baseAmount' => 0, + 'originalAmount' => 0, + 'baseOriginalAmount' => 0 + ] + ) + ->getMock(); + $this->discountFactory->expects($this->any()) + ->method('create') + ->with($this->anything()) + ->will($this->returnValue($discountData)); /** - * @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleWithStopFurtherProcessing + * @var Rule|PHPUnit_Framework_MockObject_MockObject $ruleWithStopFurtherProcessing */ $ruleWithStopFurtherProcessing = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getStoreLabel', 'getCouponType', 'getRuleId', '__wakeup', 'getActions'] ); - /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleThatShouldNotBeRun */ + /** + * @var Rule|PHPUnit_Framework_MockObject_MockObject $ruleThatShouldNotBeRun + */ $ruleThatShouldNotBeRun = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getStopRulesProcessing', '__wakeup'] ); - $actionMock = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']); + $actionMock = $this->createPartialMock(Collection::class, ['validate']); $ruleWithStopFurtherProcessing->setName('ruleWithStopFurtherProcessing'); $ruleThatShouldNotBeRun->setName('ruleThatShouldNotBeRun'); @@ -140,6 +181,40 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $this->assertEquals($appliedRuleIds, $result); } + public function testAddCouponDescriptionWithRuleDescriptionIsUsed() + { + $ruleId = 1; + $ruleDescription = 'Rule description'; + + /** + * @var Rule|PHPUnit_Framework_MockObject_MockObject $rule + */ + $rule = $this->createPartialMock( + Rule::class, + ['getStoreLabel', 'getCouponType', 'getRuleId', '__wakeup', 'getActions'] + ); + + $rule->setDescription($ruleDescription); + + /** + * @var Address|PHPUnit_Framework_MockObject_MockObject $address + */ + $address = $this->createPartialMock( + Address::class, + [ + 'getQuote', + 'setCouponCode', + 'setAppliedRuleIds', + '__wakeup' + ] + ); + $description = $address->getDiscountDescriptionArray(); + $description[$ruleId] = $rule->getDescription(); + $address->setDiscountDescriptionArray($description[$ruleId]); + + $this->assertEquals($address->getDiscountDescriptionArray(), $description[$ruleId]); + } + /** * @return array */ @@ -152,29 +227,48 @@ public function dataProviderChildren() } /** - * @return \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject + * @return AbstractItem|PHPUnit_Framework_MockObject_MockObject */ protected function getPreparedItem() { - /** @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject $address */ - $address = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ + /** + * @var Address|PHPUnit_Framework_MockObject_MockObject $address + */ + $address = $this->createPartialMock( + Address::class, + [ 'getQuote', 'setCouponCode', 'setAppliedRuleIds', '__wakeup' - ]); - /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ - $item = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, [ + ] + ); + /** + * @var AbstractItem|PHPUnit_Framework_MockObject_MockObject $item + */ + $item = $this->createPartialMock( + Item::class, + [ 'setDiscountAmount', 'setBaseDiscountAmount', 'setDiscountPercent', 'getAddress', 'setAppliedRuleIds', '__wakeup', - 'getChildren' - ]); + 'getChildren', + 'getExtensionAttributes' + ] + ); + $itemExtension = $this->getMockBuilder( + \Magento\Framework\Api\ExtensionAttributesInterface::class + )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + $itemExtension->method('getDiscounts')->willReturn([]); + $itemExtension->expects($this->any()) + ->method('setDiscounts') + ->willReturn([]); $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStore', '__wakeUp']); $item->expects($this->any())->method('getAddress')->will($this->returnValue($address)); + $item->expects($this->any())->method('getExtensionAttributes')->will($this->returnValue($itemExtension)); $address->expects($this->any()) ->method('getQuote') ->will($this->returnValue($quote)); @@ -190,10 +284,10 @@ protected function applyRule($item, $rule) { $qty = 2; $discountCalc = $this->createPartialMock( - \Magento\SalesRule\Model\Rule\Action\Discount\DiscountInterface::class, + DiscountInterface::class, ['fixQuantity', 'calculate'] ); - $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class) + $discountData = $this->getMockBuilder(Data::class) ->setConstructorArgs( [ 'amount' => 30, diff --git a/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php new file mode 100644 index 0000000000000..2fe2069b0c8b1 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php @@ -0,0 +1,175 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Test\Unit\Observer; + +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Api\Exception\CodeRequestLimitException; +use Magento\SalesRule\Model\Spi\CodeLimitManagerInterface; +use Magento\SalesRule\Observer\CouponCodeValidation; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * Class CouponCodeValidationTest + */ +class CouponCodeValidationTest extends TestCase +{ + /** + * @var CouponCodeValidation + */ + private $couponCodeValidation; + + /** + * @var PHPUnit_Framework_MockObject_MockObject|CodeLimitManagerInterface + */ + private $codeLimitManagerMock; + + /** + * @var PHPUnit_Framework_MockObject_MockObject|CartRepositoryInterface + */ + private $cartRepositoryMock; + + /** + * @var PHPUnit_Framework_MockObject_MockObject|SearchCriteriaBuilder + */ + private $searchCriteriaBuilderMock; + + /** + * @var PHPUnit_Framework_MockObject_MockObject|Observer + */ + private $observerMock; + + /** + * @var PHPUnit_Framework_MockObject_MockObject + */ + private $searchCriteriaMock; + + /** + * @var PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->codeLimitManagerMock = $this->createMock(CodeLimitManagerInterface::class); + $this->observerMock = $this->createMock(Observer::class); + $this->searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->cartRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) + ->setMethods(['getItems']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->setMethods(['addFilter', 'create']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['getCouponCode', 'setCouponCode', 'getId'] + ); + + $this->couponCodeValidation = new CouponCodeValidation( + $this->codeLimitManagerMock, + $this->cartRepositoryMock, + $this->searchCriteriaBuilderMock + ); + } + + /** + * Testing the coupon code that haven't reached the request limit + */ + public function testCouponCodeNotReachedTheLimit() + { + $couponCode = 'AB123'; + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->searchCriteriaBuilderMock->expects($this->once())->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create') + ->willReturn($this->searchCriteriaMock); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(123); + $this->cartRepositoryMock->expects($this->any())->method('getList')->willReturnSelf(); + $this->cartRepositoryMock->expects($this->any())->method('getItems')->willReturn([]); + $this->codeLimitManagerMock->expects($this->once())->method('checkRequest')->with($couponCode); + $this->quoteMock->expects($this->never())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } + + /** + * Testing with the changed coupon code + */ + public function testCouponCodeNotReachedTheLimitWithNewCouponCode() + { + $couponCode = 'AB123'; + $newCouponCode = 'AB234'; + + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->searchCriteriaBuilderMock->expects($this->once())->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create') + ->willReturn($this->searchCriteriaMock); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(123); + $this->cartRepositoryMock->expects($this->any())->method('getList')->willReturnSelf(); + $this->cartRepositoryMock->expects($this->any())->method('getItems') + ->willReturn([new DataObject(['coupon_code' => $newCouponCode])]); + $this->codeLimitManagerMock->expects($this->once())->method('checkRequest')->with($couponCode); + $this->quoteMock->expects($this->never())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } + + /** + * Testing the coupon code that reached the request limit + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + * @expectedExceptionMessage Too many coupon code requests, please try again later. + */ + public function testReachingLimitForCouponCode() + { + $couponCode = 'AB123'; + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->searchCriteriaBuilderMock->expects($this->once())->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->once())->method('create') + ->willReturn($this->searchCriteriaMock); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(123); + $this->cartRepositoryMock->expects($this->any())->method('getList')->willReturnSelf(); + $this->cartRepositoryMock->expects($this->any())->method('getItems')->willReturn([]); + $this->codeLimitManagerMock->expects($this->once())->method('checkRequest')->with($couponCode) + ->willThrowException( + new CodeRequestLimitException(__('Too many coupon code requests, please try again later.')) + ); + $this->quoteMock->expects($this->once())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } + + /** + * Testing the quote that doesn't have a coupon code set + */ + public function testQuoteWithNoCouponCode() + { + $couponCode = null; + $this->observerMock->expects($this->once())->method('getData')->with('quote') + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getCouponCode')->willReturn($couponCode); + $this->quoteMock->expects($this->never())->method('getId')->willReturn(123); + $this->quoteMock->expects($this->never())->method('setCouponCode')->with(''); + + $this->couponCodeValidation->execute($this->observerMock); + } +} diff --git a/app/code/Magento/SalesRule/etc/db_schema.xml b/app/code/Magento/SalesRule/etc/db_schema.xml index 5a4877bbf825e..e100121bea345 100644 --- a/app/code/Magento/SalesRule/etc/db_schema.xml +++ b/app/code/Magento/SalesRule/etc/db_schema.xml @@ -58,7 +58,7 @@ </table> <table name="salesrule_coupon" resource="default" engine="innodb" comment="Salesrule Coupon"> <column xsi:type="int" name="coupon_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Coupon Id"/> + comment="Coupon ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> @@ -94,9 +94,9 @@ </table> <table name="salesrule_coupon_usage" resource="default" engine="innodb" comment="Salesrule Coupon Usage"> <column xsi:type="int" name="coupon_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Coupon Id"/> + comment="Coupon ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="times_used" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Times Used"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -115,11 +115,11 @@ </table> <table name="salesrule_customer" resource="default" engine="innodb" comment="Salesrule Customer"> <column xsi:type="int" name="rule_customer_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Customer Id"/> + comment="Rule Customer ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Id"/> + default="0" comment="Customer ID"/> <column xsi:type="smallint" name="times_used" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Times Used"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -141,11 +141,11 @@ </table> <table name="salesrule_label" resource="default" engine="innodb" comment="Salesrule Label"> <column xsi:type="int" name="label_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Label Id"/> + comment="Label ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="label" nullable="true" length="255" comment="Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="label_id"/> @@ -166,11 +166,11 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> @@ -200,10 +200,10 @@ </index> </table> <table name="salesrule_coupon_aggregated" resource="sales" engine="innodb" comment="Coupon Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" @@ -242,10 +242,10 @@ </table> <table name="salesrule_coupon_aggregated_updated" resource="sales" engine="innodb" comment="Salesrule Coupon Aggregated Updated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" @@ -284,10 +284,10 @@ </table> <table name="salesrule_coupon_aggregated_order" resource="default" engine="innodb" comment="Coupon Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" @@ -322,7 +322,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> @@ -341,7 +341,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index c1d22a04771ab..abb581175e36a 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -13,7 +13,7 @@ <preference for="Magento\SalesRule\Api\Data\ConditionInterface" type="Magento\SalesRule\Model\Data\Condition" /> <preference for="Magento\SalesRule\Api\Data\RuleSearchResultInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\SalesRule\Model\RuleSearchResult" /> <preference for="Magento\SalesRule\Api\Data\RuleLabelInterface" type="Magento\SalesRule\Model\Data\RuleLabel" /> <preference for="Magento\SalesRule\Api\Data\CouponInterface" @@ -23,13 +23,17 @@ <preference for="Magento\SalesRule\Model\Spi\CouponResourceInterface" type="Magento\SalesRule\Model\ResourceModel\Coupon" /> <preference for="Magento\SalesRule\Api\Data\CouponSearchResultInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\SalesRule\Model\CouponSearchResult" /> <preference for="Magento\SalesRule\Api\Data\CouponGenerationSpecInterface" type="Magento\SalesRule\Model\Data\CouponGenerationSpec" /> <preference for="Magento\SalesRule\Api\Data\CouponMassDeleteResultInterface" type="Magento\SalesRule\Model\Data\CouponMassDeleteResult" /> <preference for="Magento\SalesRule\Api\CouponManagementInterface" type="Magento\SalesRule\Model\Service\CouponManagementService" /> + <preference for="Magento\SalesRule\Api\Data\RuleDiscountInterface" + type="Magento\SalesRule\Model\Data\RuleDiscount" /> + <preference for="Magento\SalesRule\Api\Data\DiscountDataInterface" + type="Magento\SalesRule\Model\Data\DiscountData" /> <type name="Magento\SalesRule\Helper\Coupon"> <arguments> <argument name="couponParameters" xsi:type="array"> @@ -178,7 +182,6 @@ </argument> </arguments> </type> - <type name="Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="coupon_label_plugin" type="Magento\SalesRule\Plugin\CartTotalRepository" /> </type> diff --git a/app/code/Magento/SalesRule/etc/extension_attributes.xml b/app/code/Magento/SalesRule/etc/extension_attributes.xml new file mode 100644 index 0000000000000..c69c309d8741b --- /dev/null +++ b/app/code/Magento/SalesRule/etc/extension_attributes.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:Api/etc/extension_attributes.xsd"> + <extension_attributes for="Magento\Quote\Api\Data\CartItemInterface"> + <attribute code="discounts" type="Magento\SalesRule\Api\Data\RuleDiscountInterface[]" /> + </extension_attributes> + <extension_attributes for="Magento\Quote\Api\Data\AddressInterface"> + <attribute code="discounts" type="Magento\SalesRule\Api\Data\RuleDiscountInterface[]" /> + </extension_attributes> +</config> \ No newline at end of file diff --git a/app/code/Magento/SalesRule/i18n/en_US.csv b/app/code/Magento/SalesRule/i18n/en_US.csv index 7511d147ae224..83a5aa76ba0c8 100644 --- a/app/code/Magento/SalesRule/i18n/en_US.csv +++ b/app/code/Magento/SalesRule/i18n/en_US.csv @@ -22,6 +22,7 @@ Conditions,Conditions Generate,Generate "Coupon Code","Coupon Code" Created,Created +Used,Used Uses,Uses No,No Yes,Yes diff --git a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml index 639e12006232b..e1c12f45012ee 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml @@ -297,6 +297,9 @@ </item> </argument> <settings> + <validation> + <rule name="validate-digits" xsi:type="boolean">true</rule> + </validation> <dataType>text</dataType> <label translate="true">Uses per Coupon</label> <dataScope>uses_per_coupon</dataScope> @@ -309,6 +312,9 @@ </item> </argument> <settings> + <validation> + <rule name="validate-digits" xsi:type="boolean">true</rule> + </validation> <notice translate="true"> Usage limit enforced for logged in customers only. </notice> @@ -356,6 +362,9 @@ </item> </argument> <settings> + <validation> + <rule name="validate-digits" xsi:type="boolean">true</rule> + </validation> <dataType>text</dataType> <label translate="true">Priority</label> <dataScope>sort_order</dataScope> @@ -422,6 +431,8 @@ <settings> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> + <rule name="validate-number" xsi:type="boolean">true</rule> + <rule name="validate-zero-or-greater" xsi:type="boolean">true</rule> </validation> <dataType>text</dataType> <label translate="true">Discount Amount</label> @@ -435,6 +446,10 @@ </item> </argument> <settings> + <validation> + <rule name="validate-number" xsi:type="boolean">true</rule> + <rule name="validate-zero-or-greater" xsi:type="boolean">true</rule> + </validation> <dataType>text</dataType> <label translate="true">Maximum Qty Discount is Applied To</label> <dataScope>discount_qty</dataScope> @@ -447,6 +462,10 @@ </item> </argument> <settings> + <validation> + <rule name="validate-number" xsi:type="boolean">true</rule> + <rule name="validate-zero-or-greater" xsi:type="boolean">true</rule> + </validation> <dataType>text</dataType> <label translate="true">Discount Qty Step (Buy X)</label> <dataScope>discount_step</dataScope> diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js b/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js index 72eec1be0766c..35395434cef1e 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js +++ b/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js @@ -16,9 +16,10 @@ define([ 'Magento_Checkout/js/action/get-payment-information', 'Magento_Checkout/js/model/totals', 'mage/translate', - 'Magento_Checkout/js/model/full-screen-loader' + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Checkout/js/action/recollect-shipping-rates' ], function ($, quote, urlManager, errorProcessor, messageContainer, storage, getPaymentInformationAction, totals, $t, - fullScreenLoader + fullScreenLoader, recollectShippingRates ) { 'use strict'; @@ -56,6 +57,7 @@ define([ var deferred = $.Deferred(); totals.isLoading(true); + recollectShippingRates(); getPaymentInformationAction(deferred); $.when(deferred).done(function () { isApplied(false); diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js b/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js index 994ccf2b395d2..4dbc5820feae9 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js +++ b/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js @@ -17,9 +17,10 @@ define([ 'mage/translate', 'Magento_Checkout/js/action/get-payment-information', 'Magento_Checkout/js/model/totals', - 'Magento_Checkout/js/model/full-screen-loader' + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Checkout/js/action/recollect-shipping-rates' ], function (ko, $, quote, urlManager, errorProcessor, messageContainer, storage, $t, getPaymentInformationAction, - totals, fullScreenLoader + totals, fullScreenLoader, recollectShippingRates ) { 'use strict'; @@ -62,6 +63,7 @@ define([ isApplied(true); totals.isLoading(true); + recollectShippingRates(); getPaymentInformationAction(deferred); $.when(deferred).done(function () { fullScreenLoader.stopLoader(); diff --git a/app/code/Magento/SalesSequence/etc/db_schema.xml b/app/code/Magento/SalesSequence/etc/db_schema.xml index 7ad48badf7b80..5ae72319c5a69 100644 --- a/app/code/Magento/SalesSequence/etc/db_schema.xml +++ b/app/code/Magento/SalesSequence/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="sales_sequence_profile" resource="sales" engine="innodb" comment="sales_sequence_profile" onCreate="skip-migration"> <column xsi:type="int" name="profile_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Id"/> + comment="ID"/> <column xsi:type="int" name="meta_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Meta_id"/> <column xsi:type="varchar" name="prefix" nullable="true" length="32" comment="Prefix"/> @@ -37,10 +37,10 @@ </table> <table name="sales_sequence_meta" resource="sales" engine="innodb" comment="sales_sequence_meta" onCreate="skip-migration"> <column xsi:type="int" name="meta_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Id"/> + comment="ID"/> <column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Prefix"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="sequence_table" nullable="false" length="64" comment="table for sequence"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="meta_id"/> diff --git a/app/code/Magento/Search/Block/Adminhtml/Dashboard/Last.php b/app/code/Magento/Search/Block/Adminhtml/Dashboard/Last.php index ad8d247b2a6fb..ff49f4a15b06a 100644 --- a/app/code/Magento/Search/Block/Adminhtml/Dashboard/Last.php +++ b/app/code/Magento/Search/Block/Adminhtml/Dashboard/Last.php @@ -24,7 +24,7 @@ class Last extends \Magento\Backend\Block\Dashboard\Grid protected $_queriesFactory; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; @@ -36,14 +36,14 @@ class Last extends \Magento\Backend\Block\Dashboard\Grid /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Search\Model\ResourceModel\Query\CollectionFactory $queriesFactory * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Search\Model\ResourceModel\Query\CollectionFactory $queriesFactory, array $data = [] ) { diff --git a/app/code/Magento/Search/Block/Adminhtml/Dashboard/Top.php b/app/code/Magento/Search/Block/Adminhtml/Dashboard/Top.php index 63893f788673d..3e10e1137d6b6 100644 --- a/app/code/Magento/Search/Block/Adminhtml/Dashboard/Top.php +++ b/app/code/Magento/Search/Block/Adminhtml/Dashboard/Top.php @@ -24,7 +24,7 @@ class Top extends \Magento\Backend\Block\Dashboard\Grid protected $_queriesFactory; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $_moduleManager; @@ -36,14 +36,14 @@ class Top extends \Magento\Backend\Block\Dashboard\Grid /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Search\Model\ResourceModel\Query\CollectionFactory $queriesFactory * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\Search\Model\ResourceModel\Query\CollectionFactory $queriesFactory, array $data = [] ) { diff --git a/app/code/Magento/Search/Model/QueryFactory.php b/app/code/Magento/Search/Model/QueryFactory.php index 2122451742402..4186b5c3055f4 100644 --- a/app/code/Magento/Search/Model/QueryFactory.php +++ b/app/code/Magento/Search/Model/QueryFactory.php @@ -5,13 +5,15 @@ */ namespace Magento\Search\Model; -use Magento\Search\Helper\Data; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Helper\Context; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\StringUtils as StdlibString; +use Magento\Search\Helper\Data; /** + * Search Query Factory + * * @api * @since 100.0.2 */ @@ -72,7 +74,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function get() { @@ -82,9 +84,7 @@ public function get() $rawQueryText = $this->getRawQueryText(); $preparedQueryText = $this->getPreparedQueryText($rawQueryText, $maxQueryLength); $query = $this->create()->loadByQueryText($preparedQueryText); - if (!$query->getId()) { - $query->setQueryText($preparedQueryText); - } + $query->setQueryText($preparedQueryText); $query->setIsQueryTextExceeded($this->isQueryTooLong($rawQueryText, $maxQueryLength)); $query->setIsQueryTextShort($this->isQueryTooShort($rawQueryText, $minQueryLength)); $this->query = $query; @@ -117,6 +117,8 @@ private function getRawQueryText() } /** + * Prepare query text + * * @param string $queryText * @param int|string $maxQueryLength * @return string @@ -130,6 +132,8 @@ private function getPreparedQueryText($queryText, $maxQueryLength) } /** + * Check if the provided text exceeds the provided length + * * @param string $queryText * @param int|string $maxQueryLength * @return bool @@ -140,6 +144,8 @@ private function isQueryTooLong($queryText, $maxQueryLength) } /** + * Check if the provided text is shorter than the provided length + * * @param string $queryText * @param int|string $minQueryLength * @return bool diff --git a/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php b/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php index e34c841f32bd1..a1bc1df3f9bdb 100644 --- a/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php +++ b/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php @@ -130,7 +130,6 @@ public function setQueryFilter($query) */ public function setPopularQueryFilter($storeIds = null) { - $this->getSelect()->reset( \Magento\Framework\DB\Select::FROM )->reset( @@ -140,13 +139,10 @@ public function setPopularQueryFilter($storeIds = null) )->from( ['main_table' => $this->getTable('search_query')] ); - if ($storeIds) { - $this->addStoreFilter($storeIds); - $this->getSelect()->where('num_results > 0'); - } elseif (null === $storeIds) { - $this->addStoreFilter($this->_storeManager->getStore()->getId()); - $this->getSelect()->where('num_results > 0'); - } + + $storeIds = $storeIds ?: $this->_storeManager->getStore()->getId(); + $this->addStoreFilter($storeIds); + $this->getSelect()->where('num_results > 0'); $this->getSelect()->order(['popularity desc']); @@ -172,10 +168,9 @@ public function setRecentQueryFilter() */ public function addStoreFilter($storeIds) { - if (!is_array($storeIds)) { - $storeIds = [$storeIds]; - } - $this->getSelect()->where('main_table.store_id IN (?)', $storeIds); + $condition = is_array($storeIds) ? 'main_table.store_id IN (?)' : 'main_table.store_id = ?'; + $this->getSelect()->where($condition, $storeIds); + return $this; } } diff --git a/app/code/Magento/Search/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Search/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..4a742b290c983 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,16 @@ +<?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="SearchEngineMysqlConfigData"> + <data key="path">catalog/search/engine</data> + <data key="scope_id">1</data> + <data key="label">MySQL</data> + <data key="value">mysql</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Data/SearchEngineConfigData.xml b/app/code/Magento/Search/Test/Mftf/Data/SearchEngineConfigData.xml new file mode 100644 index 0000000000000..7a6eb86a5cf52 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/SearchEngineConfigData.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="SetDefaultSearchEngineConfig"> + <data key="path">catalog/search/engine</data> + <data key="value">mysql</data> + </entity> + <entity name="SetMinQueryLength3Config"> + <data key="path">catalog/search/min_query_length</data> + <data key="value">3</data> + </entity> + <entity name="SetMinQueryLength2Config"> + <data key="path">catalog/search/min_query_length</data> + <data key="value">2</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Search/Test/Mftf/Suite/SearchEngineMysqlSuite.xml b/app/code/Magento/Search/Test/Mftf/Suite/SearchEngineMysqlSuite.xml new file mode 100644 index 0000000000000..9ed6ccda62200 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Suite/SearchEngineMysqlSuite.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="SearchEngineMysqlSuite"> + <before> + <magentoCLI stepKey="setSearchEngineToMysql" command="config:set {{SearchEngineMysqlConfigData.path}} {{SearchEngineMysqlConfigData.value}}"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after></after> + <include> + <group name="SearchEngineMysql" /> + </include> + <exclude> + <group name="skip"/> + </exclude> + </suite> +</suites> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 0ec33c48f259e..c5124ac9c74a1 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -28,9 +28,14 @@ <!-- Delete all search terms --> <comment userInput="Delete all search terms" stepKey="deleteAllSearchTermsComment"/> <actionGroup ref="DeleteAllSearchTerms" stepKey="deleteAllSearchTerms"/> + <actionGroup ref="deleteAllProductsUsingProductGrid" stepKey="deleteAllProducts"/> <!-- Create product with description --> <comment userInput="Create product with description" stepKey="createProductWithDescriptionComment"/> <createData entity="SimpleProductWithDescription" stepKey="simpleProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <!-- Delete created product --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml index 119faef9f2f59..e49db08954e14 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -24,6 +24,10 @@ <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <!-- Delete create product --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index ca48fb8565ca8..a1aa8be999aea 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -24,6 +24,10 @@ <!-- Create product with short description --> <createData entity="ApiProductWithDescription" stepKey="product"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml index 6033ea8dee28b..3a8443706c9c7 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -24,6 +24,10 @@ <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php b/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php index 3df457b0d4497..f66c1c7dd9e3f 100644 --- a/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php @@ -5,14 +5,14 @@ */ namespace Magento\Search\Test\Unit\Model; -use Magento\Search\Helper\Data; use Magento\Framework\App\Helper\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Search\Model\QueryFactory; use Magento\Framework\Stdlib\StringUtils; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Search\Helper\Data; use Magento\Search\Model\Query; +use Magento\Search\Model\QueryFactory; /** * Class QueryFactoryTest tests Magento\Search\Model\QueryFactory @@ -67,7 +67,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->query = $this->getMockBuilder(Query::class) - ->setMethods(['setIsQueryTextExceeded', 'setIsQueryTextShort', 'loadByQueryText', 'getId', 'setQueryText']) + ->setMethods(['setIsQueryTextExceeded', 'setIsQueryTextShort', 'loadByQueryText', 'getId']) ->disableOriginalConstructor() ->getMock(); @@ -124,7 +124,6 @@ public function testGetNewQuery() $isQueryTextExceeded = false; $isQueryTextShort = false; - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -135,6 +134,7 @@ public function testGetNewQuery() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); } /** @@ -150,7 +150,6 @@ public function testGetQueryTwice() $isQueryTextExceeded = false; $isQueryTextShort = false; - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -163,6 +162,7 @@ public function testGetQueryTwice() $result = $this->model->get(); $this->assertSame($this->query, $result, 'After second execution queries are not same'); + $this->assertSearchQuery($cleanedRawText); } /** @@ -184,7 +184,6 @@ public function testGetTooLongQuery() ->withConsecutive([$cleanedRawText, 0, $maxQueryLength]) ->willReturn($subRawText); - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -194,6 +193,7 @@ public function testGetTooLongQuery() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($subRawText); } /** @@ -209,7 +209,6 @@ public function testGetTooShortQuery() $isQueryTextExceeded = false; $isQueryTextShort = true; - $this->mockSetQueryTextNeverExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -219,6 +218,7 @@ public function testGetTooShortQuery() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); } /** @@ -234,7 +234,6 @@ public function testGetQueryWithoutId() $isQueryTextExceeded = false; $isQueryTextShort = false; - $this->mockSetQueryTextOnceExecute($cleanedRawText); $this->mockString($cleanedRawText); $this->mockQueryLengths($maxQueryLength, $minQueryLength); $this->mockGetRawQueryText($rawQueryText); @@ -244,6 +243,35 @@ public function testGetQueryWithoutId() $result = $this->model->get(); $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); + } + + /** + * Test for inaccurate match of search query in query_text table + * + * Because of inaccurate string comparison of utf8_general_ci, + * the search_query result text may be different from the original text (e.g organos, Organos, Órganos) + */ + public function testInaccurateQueryTextMatch() + { + $queryId = 1; + $maxQueryLength = 100; + $minQueryLength = 3; + $rawQueryText = 'Órganos'; + $cleanedRawText = 'Órganos'; + $isQueryTextExceeded = false; + $isQueryTextShort = false; + + $this->mockString($cleanedRawText); + $this->mockQueryLengths($maxQueryLength, $minQueryLength); + $this->mockGetRawQueryText($rawQueryText); + $this->mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded, $isQueryTextShort, 'Organos'); + + $this->mockCreateQuery(); + + $result = $this->model->get(); + $this->assertSame($this->query, $result); + $this->assertSearchQuery($cleanedRawText); } /** @@ -305,15 +333,25 @@ private function mockCreateQuery() * @param int $queryId * @param bool $isQueryTextExceeded * @param bool $isQueryTextShort + * @param string $matchedQueryText * @return void */ - private function mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded, $isQueryTextShort) - { + private function mockSimpleQuery( + string $cleanedRawText, + ?int $queryId, + bool $isQueryTextExceeded, + bool $isQueryTextShort, + string $matchedQueryText = null + ) { + if (null === $matchedQueryText) { + $matchedQueryText = $cleanedRawText; + } $this->query->expects($this->once()) ->method('loadByQueryText') ->withConsecutive([$cleanedRawText]) ->willReturnSelf(); - $this->query->expects($this->once()) + $this->query->setData(['query_text' => $matchedQueryText]); + $this->query->expects($this->any()) ->method('getId') ->willReturn($queryId); $this->query->expects($this->once()) @@ -328,23 +366,8 @@ private function mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded * @param string $cleanedRawText * @return void */ - private function mockSetQueryTextNeverExecute($cleanedRawText) + private function assertSearchQuery($cleanedRawText) { - $this->query->expects($this->never()) - ->method('setQueryText') - ->withConsecutive([$cleanedRawText]) - ->willReturnSelf(); - } - - /** - * @param string $cleanedRawText - * @return void - */ - private function mockSetQueryTextOnceExecute($cleanedRawText) - { - $this->query->expects($this->once()) - ->method('setQueryText') - ->withConsecutive([$cleanedRawText]) - ->willReturnSelf(); + $this->assertEquals($cleanedRawText, $this->query->getQueryText()); } } diff --git a/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php b/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php index 982d00bd9f648..748e441652f72 100644 --- a/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php @@ -50,6 +50,9 @@ public function testGetPageSize($searchEngine, $size) $this->assertEquals($size, $this->model->getMaxPageSize()); } + /** + * @return array + */ public function getPageSizeDataProvider() { return [ diff --git a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php index 8cc9b809ff888..f42ce50d2804b 100644 --- a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php +++ b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php @@ -63,11 +63,13 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete'), 'message' => __('Are you sure you want to delete synonym group with id: %1?', $item['group_id']) - ] + ], + '__disableTmpl' => true ]; $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl(self::SYNONYM_URL_PATH_EDIT, ['group_id' => $item['group_id']]), 'label' => __('View/Edit'), + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Search/etc/db_schema.xml b/app/code/Magento/Search/etc/db_schema.xml index 754af7d246d6d..1a01ffa42401c 100644 --- a/app/code/Magento/Search/etc/db_schema.xml +++ b/app/code/Magento/Search/etc/db_schema.xml @@ -46,15 +46,19 @@ <index referenceId="SEARCH_QUERY_IS_PROCESSED" indexType="btree"> <column name="is_processed"/> </index> + <index referenceId="SEARCH_QUERY_STORE_ID_POPULARITY" indexType="btree"> + <column name="store_id"/> + <column name="popularity"/> + </index> </table> <table name="search_synonyms" resource="default" engine="innodb" comment="table storing various synonyms groups"> <column xsi:type="bigint" name="group_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Synonyms Group Id"/> + comment="Synonyms Group ID"/> <column xsi:type="text" name="synonyms" nullable="false" comment="list of synonyms making up this group"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id - identifies the store view these synonyms belong to"/> + default="0" comment="Store ID - identifies the store view these synonyms belong to"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id - identifies the website id these synonyms belong to"/> + default="0" comment="Website ID - identifies the website ID these synonyms belong to"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="group_id"/> </constraint> diff --git a/app/code/Magento/Search/etc/db_schema_whitelist.json b/app/code/Magento/Search/etc/db_schema_whitelist.json index 71adbc68887d0..16bbd0ce9fa3c 100644 --- a/app/code/Magento/Search/etc/db_schema_whitelist.json +++ b/app/code/Magento/Search/etc/db_schema_whitelist.json @@ -17,7 +17,8 @@ "SEARCH_QUERY_QUERY_TEXT_STORE_ID_POPULARITY": true, "SEARCH_QUERY_STORE_ID": true, "SEARCH_QUERY_IS_PROCESSED": true, - "SEARCH_QUERY_SYNONYM_FOR": true + "SEARCH_QUERY_SYNONYM_FOR": true, + "SEARCH_QUERY_STORE_ID_POPULARITY": true }, "constraint": { "PRIMARY": true, @@ -43,4 +44,4 @@ "SEARCH_SYNONYMS_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml b/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml index 38c6fa52455b9..e7f31097368e2 100644 --- a/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml +++ b/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml @@ -81,16 +81,7 @@ <argument name="sortable" xsi:type="string">1</argument> <argument name="index" xsi:type="string">display_in_terms</argument> <argument name="type" xsi:type="string">options</argument> - <argument name="options" xsi:type="array"> - <item name="yes" xsi:type="array"> - <item name="value" xsi:type="string">1</item> - <item name="label" xsi:type="string" translate="true">yes</item> - </item> - <item name="no" xsi:type="array"> - <item name="value" xsi:type="string">0</item> - <item name="label" xsi:type="string" translate="true">no</item> - </item> - </argument> + <argument name="options" xsi:type="options" model="Magento\Config\Model\Config\Source\Yesno"/> </arguments> </block> <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.catalog.search.grid.columnSet.action" as="action"> diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 766dd4d992bd4..0dd9c819c855a 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -24,7 +24,8 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); data-mage-init='{"quickSearch":{ "formSelector":"#search_mini_form", "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", - "destinationSelector":"#search_autocomplete"} + "destinationSelector":"#search_autocomplete", + "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} }' type="text" name="<?= $block->escapeHtmlAttr($helper->getQueryParamName()) ?>" diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index 64e6eceb1eba6..b4493c5f38089 100644 --- a/app/code/Magento/Search/view/frontend/web/js/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -30,7 +30,7 @@ define([ $.widget('mage.quickSearch', { options: { autocomplete: 'off', - minSearchLength: 2, + minSearchLength: 3, responseFieldElements: 'ul li', selectClass: 'selected', template: diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenEditingUserTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenEditingUserTest.xml index 2ce54d6c0fda5..9f421668bdc4f 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenEditingUserTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenEditingUserTest.xml @@ -20,14 +20,7 @@ <group value="mtf_migrated"/> </annotations> <before> - <!-- - @TODO: Remove "executeJS" in scope of MQE-1561 - Hack to be able to pass current admin user password without hardcoding it. - --> - <executeJS function="return '{{DefaultAdminUser.password}}'" stepKey="adminPassword" /> - <createData entity="NewAdminUser" stepKey="user"> - <field key="current_password">{$adminPassword}</field> - </createData> + <createData entity="NewAdminUser" stepKey="user" /> <!-- Log in to Admin Panel --> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml index ce7143582ce69..5052f5642cb53 100644 --- a/app/code/Magento/Security/etc/db_schema.xml +++ b/app/code/Magento/Security/etc/db_schema.xml @@ -10,7 +10,7 @@ <table name="admin_user_session" resource="default" engine="innodb" comment="Admin User sessions table"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="varchar" name="session_id" nullable="false" length="128" comment="Session id value"/> + <column xsi:type="varchar" name="session_id" nullable="false" length="128" comment="Session ID value"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Admin User ID"/> <column xsi:type="smallint" name="status" padding="5" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/SendFriend/Test/Unit/Model/CaptchaValidatorTest.php b/app/code/Magento/SendFriend/Test/Unit/Model/CaptchaValidatorTest.php new file mode 100644 index 0000000000000..22377897e564a --- /dev/null +++ b/app/code/Magento/SendFriend/Test/Unit/Model/CaptchaValidatorTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SendFriend\Test\Unit\Model; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\SendFriend\Model\CaptchaValidator; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * Test CaptchaValidatorTest + */ +class CaptchaValidatorTest extends TestCase +{ + const FORM_ID = 'product_sendtofriend_form'; + + /** + * @var CaptchaValidator + */ + private $model; + + /** + * @var CaptchaStringResolver|PHPUnit_Framework_MockObject_MockObject + */ + private $captchaStringResolverMock; + + /** + * @var UserContextInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $currentUserMock; + + /** + * @var CustomerRepositoryInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var Data|PHPUnit_Framework_MockObject_MockObject + */ + private $captchaHelperMock; + + /** + * @var DefaultModel|PHPUnit_Framework_MockObject_MockObject + */ + private $captchaMock; + + /** + * @var RequestInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * Set Up + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->captchaHelperMock = $this->createMock(Data::class); + $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); + $this->currentUserMock = $this->getMockBuilder(UserContextInterface::class) + ->getMockForAbstractClass(); + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); + $this->captchaMock = $this->createMock(DefaultModel::class); + $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMock(); + + $this->model = $objectManager->getObject( + CaptchaValidator::class, + [ + 'captchaHelper' => $this->captchaHelperMock, + 'captchaStringResolver' => $this->captchaStringResolverMock, + 'currentUser' => $this->currentUserMock, + 'customerRepository' => $this->customerRepositoryMock, + ] + ); + } + + /** + * Testing the captcha validation before sending the email + * + * @dataProvider captchaProvider + * + * @param bool $captchaIsRequired + * @param bool $captchaWordIsValid + * + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function testCaptchaValidationOnSend(bool $captchaIsRequired, bool $captchaWordIsValid) + { + $word = 'test-word'; + $this->captchaHelperMock->expects($this->once())->method('getCaptcha')->with(static::FORM_ID) + ->will($this->returnValue($this->captchaMock)); + $this->captchaMock->expects($this->once())->method('isRequired') + ->will($this->returnValue($captchaIsRequired)); + + if ($captchaIsRequired) { + $this->captchaStringResolverMock->expects($this->once())->method('resolve') + ->with($this->requestMock, static::FORM_ID)->will($this->returnValue($word)); + $this->captchaMock->expects($this->once())->method('isCorrect')->with($word) + ->will($this->returnValue($captchaWordIsValid)); + } + + $this->model->validateSending($this->requestMock); + } + + /** + * Testing the wrong used word for captcha + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Incorrect CAPTCHA + */ + public function testWrongCaptcha() + { + $word = 'test-word'; + $captchaIsRequired = true; + $captchaWordIsCorrect = false; + $this->captchaHelperMock->expects($this->once())->method('getCaptcha')->with(static::FORM_ID) + ->will($this->returnValue($this->captchaMock)); + $this->captchaMock->expects($this->once())->method('isRequired') + ->will($this->returnValue($captchaIsRequired)); + $this->captchaStringResolverMock->expects($this->any())->method('resolve') + ->with($this->requestMock, static::FORM_ID)->will($this->returnValue($word)); + $this->captchaMock->expects($this->any())->method('isCorrect')->with($word) + ->will($this->returnValue($captchaWordIsCorrect)); + + $this->model->validateSending($this->requestMock); + } + + /** + * Providing captcha settings + * + * @return array + */ + public function captchaProvider(): array + { + return [ + [ + true, + true + ], [ + false, + false + ] + ]; + } +} diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index f06f1b4a9e3e3..064b45e97d6c5 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -11,7 +11,8 @@ "magento/module-customer": "*", "magento/module-store": "*", "magento/module-captcha": "*", - "magento/module-authorization": "*" + "magento/module-authorization": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SendFriend/etc/adminhtml/system.xml b/app/code/Magento/SendFriend/etc/adminhtml/system.xml index 785b7a8bb40c8..5cace4bcf92db 100644 --- a/app/code/Magento/SendFriend/etc/adminhtml/system.xml +++ b/app/code/Magento/SendFriend/etc/adminhtml/system.xml @@ -13,8 +13,11 @@ <resource>Magento_Config::sendfriend</resource> <group id="email" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Email Templates</label> - <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="enabled" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enabled</label> + <comment> + <![CDATA[We strongly recommend to enable a <a href="https://devdocs.magento.com/guides/v2.3/security/google-recaptcha.html" target="_blank">CAPTCHA solution</a> alongside enabling "Email to a Friend" to ensure abuse of this feature does not occur.]]> + </comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="template" translate="label comment" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> diff --git a/app/code/Magento/SendFriend/etc/config.xml b/app/code/Magento/SendFriend/etc/config.xml index d65e5a4a073dd..6239a4da591e2 100644 --- a/app/code/Magento/SendFriend/etc/config.xml +++ b/app/code/Magento/SendFriend/etc/config.xml @@ -9,7 +9,7 @@ <default> <sendfriend> <email> - <enabled>1</enabled> + <enabled>0</enabled> <template>sendfriend_email_template</template> <allow_guest>0</allow_guest> <max_recipients>5</max_recipients> diff --git a/app/code/Magento/SendFriend/i18n/en_US.csv b/app/code/Magento/SendFriend/i18n/en_US.csv index eee540c89a7b0..8d5b596fe1ca6 100644 --- a/app/code/Magento/SendFriend/i18n/en_US.csv +++ b/app/code/Magento/SendFriend/i18n/en_US.csv @@ -45,3 +45,4 @@ Enabled,Enabled "Max Recipients","Max Recipients" "Max Products Sent in 1 Hour","Max Products Sent in 1 Hour" "Limit Sending By","Limit Sending By" +"We strongly recommend to enable a <a href=""https://devdocs.magento.com/guides/v2.3/security/google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur.","We strongly recommend to enable a <a href=""https://devdocs.magento.com/guides/v2.3/security/google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur." diff --git a/app/code/Magento/SendFriend/view/frontend/email/product_share.html b/app/code/Magento/SendFriend/view/frontend/email/product_share.html index d2ed441494221..00a4d7b4d5ce5 100644 --- a/app/code/Magento/SendFriend/view/frontend/email/product_share.html +++ b/app/code/Magento/SendFriend/view/frontend/email/product_share.html @@ -10,10 +10,11 @@ "var email":"Recipient Email address", "var name":"Recipient name", "var message|raw":"Sender custom message", -"var sender_email":"Sender email", -"var sender_name":"Sender name", +"var sender_email":"Sender Email", +"var sender_name":"Sender Name", "var product_url":"URL for Product", -"var product_image":"URL for product small image (75 px)" +"var product_image":"URL for product small image (75 px)", +"var message":"Message" } @--> {{template config_path="design/email/header_template"}} diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php index 356483c9a5dd7..55eecfa00d6da 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php @@ -43,7 +43,7 @@ public function __construct( */ protected function _prepareLayout() { - $onclick = "submitAndReloadArea($('shipment_tracking_info').parentNode, '" . $this->getSubmitUrl() . "')"; + $onclick = "saveTrackingInfo($('shipment_tracking_info').parentNode, '" . $this->getSubmitUrl() . "')"; $this->addChild( 'save_button', \Magento\Backend\Block\Widget\Button::class, @@ -86,7 +86,10 @@ public function getRemoveUrl($track) } /** + * Get carrier title + * * @param string $code + * * @return \Magento\Framework\Phrase|string|bool */ public function getCarrierTitle($code) diff --git a/app/code/Magento/Shipping/Model/Shipping.php b/app/code/Magento/Shipping/Model/Shipping.php index 5470f9a96775b..48127469ea984 100644 --- a/app/code/Magento/Shipping/Model/Shipping.php +++ b/app/code/Magento/Shipping/Model/Shipping.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Model; use Magento\Framework\App\ObjectManager; @@ -272,7 +273,9 @@ public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest $req */ private function prepareCarrier(string $carrierCode, RateRequest $request): AbstractCarrier { - $carrier = $this->_carrierFactory->create($carrierCode, $request->getStoreId()); + $carrier = $this->isShippingCarrierAvailable($carrierCode) + ? $this->_carrierFactory->create($carrierCode, $request->getStoreId()) + : null; if (!$carrier) { throw new \RuntimeException('Failed to initialize carrier'); } @@ -360,6 +363,7 @@ public function composePackagesForCarrier($carrier, $request) { $allItems = $request->getAllItems(); $fullItems = []; + $weightItems = []; $maxWeight = (double)$carrier->getConfigData('max_package_weight'); @@ -425,15 +429,13 @@ public function composePackagesForCarrier($carrier, $request) if (!empty($decimalItems)) { foreach ($decimalItems as $decimalItem) { - $fullItems = array_merge( - $fullItems, - array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight']) - ); + $weightItems[] = array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight']); } } else { - $fullItems = array_merge($fullItems, array_fill(0, $qty, $itemWeight)); + $weightItems[] = array_fill(0, $qty, $itemWeight); } } + $fullItems = array_merge($fullItems, ...$weightItems); sort($fullItems); return $this->_makePieces($fullItems, $maxWeight); @@ -532,4 +534,18 @@ public function setCarrierAvailabilityConfigField($code = 'active') $this->_availabilityConfigField = $code; return $this; } + + /** + * Checks availability of carrier. + * + * @param string $carrierCode + * @return bool + */ + private function isShippingCarrierAvailable(string $carrierCode): bool + { + return $this->_scopeConfig->isSetFlag( + 'carriers/' . $carrierCode . '/' . $this->_availabilityConfigField, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAddTrackingNumberToShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAddTrackingNumberToShipmentActionGroup.xml new file mode 100644 index 0000000000000..87f139c9dc770 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAddTrackingNumberToShipmentActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AdminAddTrackingNumberToShipmentActionGroup"> + <arguments> + <argument name="trackingTitle" type="string" defaultValue=""/> + <argument name="trackingNumber" type="string"/> + </arguments> + + <fillField selector="{{AdminShipmentTrackingSection.trackingTitle}}" userInput="{{trackingTitle}}" stepKey="fillTrackingTitle"/> + <fillField selector="{{AdminShipmentTrackingSection.trackingNumber}}" userInput="{{trackingNumber}}" stepKey="fillTrackingNumber"/> + <click selector="{{AdminShipmentTrackingSection.addTrackingNumber}}" stepKey="clickAddTrackingNumber"/> + <waitForPageLoad stepKey="waitForTrackingInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertCreatedShipmentInShipmentsTabActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertCreatedShipmentInShipmentsTabActionGroup.xml new file mode 100644 index 0000000000000..1a7d3355e4ee4 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertCreatedShipmentInShipmentsTabActionGroup.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="AdminAssertCreatedShipmentsInShipmentsTabActionGroup"> + <click stepKey="navigateToShipmentsTab" selector="{{AdminOrderDetailsOrderViewSection.shipments}}"/> + <waitForPageLoad stepKey="waitForTabLoad"/> + <grabTextFrom selector="{{AdminShipmentsGridSection.shipmentId}}" stepKey="grabShipmentId"/> + <assertNotEmpty actual="$grabShipmentId" stepKey="assertShipmentIdIsNotEmpty" after="grabShipmentId"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertExistingTrackingNumberActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertExistingTrackingNumberActionGroup.xml new file mode 100644 index 0000000000000..03301aa22f583 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertExistingTrackingNumberActionGroup.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="AdminAssertExistingTrackingNumberActionGroup"> + <arguments> + <argument name="trackingNumber" type="string"/> + </arguments> + + <see selector="#shipment_tracking_info .col-number" userInput="{{trackingNumber}}" stepKey="seeAvailableTrackingNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertShipmentInShipmentsGridActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertShipmentInShipmentsGridActionGroup.xml new file mode 100644 index 0000000000000..de293c24a9c5d --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertShipmentInShipmentsGridActionGroup.xml @@ -0,0 +1,27 @@ +<?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="AdminAssertShipmentInShipmentsGrid"> + <arguments> + <argument name="shipmentId" type="string"/> + </arguments> + <!--Assert Shipment in Shipments Grid--> + <amOnPage url="{{AdminShipmentsGridPage.url}}" stepKey="onShipmentsGridPage"/> + <waitForPageLoad stepKey="waitForLoadingPage"/> + <conditionalClick selector="{{AdminShipmentsGridSection.clearFilters}}" dependentSelector="{{AdminShipmentsGridSection.clearFilters}}" visible="true" stepKey="clearFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForFilterLoad"/> + <click selector="{{AdminShipmentsGridSection.buttonFilters}}" stepKey="openFilterSearch"/> + <waitForLoadingMaskToDisappear stepKey="waitForFilterFields"/> + <fillField userInput="{{shipmentId}}" selector="{{AdminShipmentsGridSection.fieldShipment}}" stepKey="fillSearchByShipmentId"/> + <click selector="{{AdminShipmentsGridSection.applyFilter}}" stepKey="clickSearchButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForSearchResult"/> + <see userInput="{{shipmentId}}" selector="{{AdminShipmentsGridSection.rowShipments}}" stepKey="seeShipmentId"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertShipmentItemsActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertShipmentItemsActionGroup.xml new file mode 100644 index 0000000000000..c4a0b4536fe20 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertShipmentItemsActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AdminAssertShipmentItemsActionGroup"> + <arguments> + <argument name="product" defaultValue="" type="string"/> + <argument name="qty" defaultValue="" type="string"/> + </arguments> + <click selector="{{AdminShipmentsGridSection.shipmentId}}" stepKey="clickView"/> + <scrollTo selector="{{AdminShipmentItemsSection.itemName('1')}}" stepKey="scrollToShippedItems"/> + <see userInput="{{product}}" selector="{{AdminShipmentItemsSection.itemName('1')}}" stepKey="seeProductName"/> + <see userInput="{{qty}}" selector="{{AdminShipmentItemsSection.productQty}}" stepKey="seeQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertTrackingValidationErrorActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertTrackingValidationErrorActionGroup.xml new file mode 100644 index 0000000000000..3783a9b1cc4db --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertTrackingValidationErrorActionGroup.xml @@ -0,0 +1,19 @@ +<?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="AdminAssertTrackingValidationErrorActionGroup"> + <arguments> + <argument name="inputName" type="string"/> + <argument name="errorMessage" type="string" defaultValue="This is a required field."/> + </arguments> + + <see selector="{{AdminShipmentTrackingSection.trackingInfoErrorElement(inputName)}}" userInput="{{errorMessage}}" stepKey="seeTrackingInfoValidationError"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminCreateShipmentFromOrderPageActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminCreateShipmentFromOrderPageActionGroup.xml new file mode 100644 index 0000000000000..0e1358651c58a --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminCreateShipmentFromOrderPageActionGroup.xml @@ -0,0 +1,29 @@ +<?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"> + <!--Create Shipment With Tracking Number--> + <actionGroup name="AdminCreateShipmentFromOrderPage"> + <arguments> + <argument name="Title" defaultValue="" type="string"/> + <argument name="Number" defaultValue="" type="string"/> + <argument name="Comment" defaultValue="" type="string"/> + <argument name="Qty" defaultValue="" type="string"/> + </arguments> + + <click stepKey="clickShipButton" selector="{{AdminOrderDetailsMainActionsSection.ship}}"/> + <click stepKey="clickAddTrackingNumber" selector="{{AdminShipmentPaymentShippingSection.AddTrackingNumber}}"/> + <fillField stepKey="fillTitle" userInput="{{Title}}" selector="{{AdminShipmentPaymentShippingSection.Title('1')}}"/> + <fillField stepKey="fillNumber" userInput="{{Number}}" selector="{{AdminShipmentPaymentShippingSection.Number('1')}}"/> + <fillField stepKey="fillQty" userInput="{{Qty}}" selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}"/> + <fillField stepKey="fillComment" userInput="{{Comment}}" selector="{{AdminShipmentTotalSection.CommentText}}"/> + <click stepKey="clickSubmitButton" selector="{{AdminShipmentMainActionsSection.submitShipment}}"/> + <see userInput="The shipment has been created." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminDeleteTrackingNumberActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminDeleteTrackingNumberActionGroup.xml new file mode 100644 index 0000000000000..8c2629293acb7 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminDeleteTrackingNumberActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AdminDeleteTrackingNumberActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Are you sure?"/> + </arguments> + + <click selector="{{AdminShipmentTrackingSection.deleteTrackingNumber}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminGridConfirmActionSection.message}}" stepKey="waitForConfirmModal"/> + <see selector="{{AdminGridConfirmActionSection.message}}" userInput="{{message}}" stepKey="seeRemoveMessage"/> + <click selector="{{AdminGridConfirmActionSection.ok}}" stepKey="clickOkButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminImportFileTableRatesShippingMethodActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminImportFileTableRatesShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..bfae3d8b76f19 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminImportFileTableRatesShippingMethodActionGroup.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="AdminImportFileTableRatesShippingMethodActionGroup"> + <annotations> + <description>Import a file in Table Rates tab in Shipping Method config page.</description> + </annotations> + <arguments> + <argument name="file" type="string" defaultValue="test_tablerates.csv"/> + </arguments> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTab"/> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="{{file}}" stepKey="attachFileForImport"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminSelectFirstGridRowActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminSelectFirstGridRowActionGroup.xml new file mode 100644 index 0000000000000..fc30d752f201f --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminSelectFirstGridRowActionGroup.xml @@ -0,0 +1,15 @@ +<?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="AdminSelectFirstGridRowActionGroup"> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickFirstRowInGrid"/> + <waitForPageLoad stepKey="waitToProcessPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml index e9809ae0f3e7f..631db885ab3d9 100644 --- a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml @@ -62,4 +62,24 @@ <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPageShipping"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> </actionGroup> + <actionGroup name="AdminShipmentCreateShippingLabelActionGroup"> + <arguments> + <argument name="productName" type="string" defaultValue="{{SimpleProduct.name}}"/> + </arguments> + <waitForElementVisible selector="{{AdminShipmentCreatePackageMainSection.addProductsToPackage}}" stepKey="waitForAddProductElement"/> + <click selector="{{AdminShipmentCreatePackageMainSection.addProductsToPackage}}" stepKey="clickAddProducts"/> + <waitForElementVisible selector="{{AdminShipmentCreatePackageProductGridSection.concreteProductCheckbox('productName')}}" stepKey="waitForProductBeVisible"/> + <checkOption selector="{{AdminShipmentCreatePackageProductGridSection.concreteProductCheckbox('productName')}}" stepKey="checkProductCheckbox"/> + <waitForElementVisible selector="{{AdminShipmentCreatePackageMainSection.addSelectedProductToPackage}}" stepKey="waitForAddSelectedProductElement"/> + <click selector="{{AdminShipmentCreatePackageMainSection.addSelectedProductToPackage}}" stepKey="clickAddSelectedProduct"/> + <waitForElementNotVisible selector="{{AdminShipmentCreatePackageMainSection.saveButtonDisabled}}" stepKey="waitForBeEnabled"/> + <click selector="{{AdminShipmentCreatePackageMainSection.save}}" stepKey="clickSave"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created. You created the shipping label." stepKey="seeShipmentCreateSuccess"/> + </actionGroup> + <actionGroup name="AdminGoToShipmentTabActionGroup"> + <click selector="{{AdminOrderDetailsOrderViewSection.shipments}}" stepKey="clickOrderShipmentsTab"/> + <waitForLoadingMaskToDisappear stepKey="waitForShipmentTabLoad" after="clickOrderShipmentsTab"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertThereIsNoShipButtonActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertThereIsNoShipButtonActionGroup.xml new file mode 100644 index 0000000000000..10521769c5070 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertThereIsNoShipButtonActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!--Create Shipment With Tracking Number--> + <actionGroup name="AssertThereIsNoShipButtonActionGroup"> + <dontSee stepKey="dontSeeShipButton" selector="{{AdminOrderDetailsMainActionsSection.ship}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/StorefrontSetShippingMethodActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/StorefrontSetShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..7fdfe6d88b8e6 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/StorefrontSetShippingMethodActionGroup.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="StorefrontSetShippingMethodActionGroup"> + <annotations> + <description>Selects the provided Shipping Method on checkout shipping and wait loading mask.</description> + </annotations> + <arguments> + <argument name="shippingMethodName" type="string" defaultValue="Flat Rate"/> + </arguments> + <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName(shippingMethodName)}}" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/AdminShippingSettingsConfigData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/AdminShippingSettingsConfigData.xml new file mode 100644 index 0000000000000..342472aab6f42 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Data/AdminShippingSettingsConfigData.xml @@ -0,0 +1,36 @@ +<?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="AdminShippingSettingsOriginCountryConfigData"> + <data key="path">shipping/origin/country_id</data> + </entity> + <entity name="AdminShippingSettingsOriginZipCodeConfigData"> + <data key="path">shipping/origin/postcode</data> + </entity> + <entity name="AdminShippingSettingsOriginCityConfigData"> + <data key="path">shipping/origin/city</data> + </entity> + <entity name="AdminShippingSettingsOriginStreetAddressConfigData"> + <data key="path">shipping/origin/street_line1</data> + </entity> + <entity name="AdminShippingSettingsOriginStreetAddress2ConfigData"> + <data key="path">shipping/origin/street_line2</data> + </entity> + <entity name="AdminFreeshippingActiveConfigData"> + <data key="path">carriers/freeshipping/active</data> + <data key="enabled">1</data> + <data key="disabled">0</data> + </entity> + <entity name="AdminFreeshippingMinimumOrderAmountConfigData"> + <data key="path">carriers/freeshipping/free_shipping_subtotal</data> + <data key="hundred">100</data> + <data key="default">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml index 6d877dac5cbf4..6ab1d71933826 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml @@ -51,7 +51,7 @@ <data key="value">5.00</data> </entity> <entity name="flatRateHandlingFeeDefault" type="handling_fee"> - <data key="value">F</data> + <data key="value">0</data> </entity> <entity name="flatRateSpecificerrmsgDefault" type="specificerrmsg"> <data key="value">This shipping method is not available. To use this shipping method, please contact us.</data> diff --git a/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentsGridPage.xml b/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentsGridPage.xml new file mode 100644 index 0000000000000..61aad55401248 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentsGridPage.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="AdminShipmentsGridPage" url="sales/shipment/" area="admin" module="Magento_Sales"> + <section name="AdminShipmentsGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentCreatePackageSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentCreatePackageSection.xml new file mode 100644 index 0000000000000..5f33921b5a44f --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentCreatePackageSection.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="AdminShipmentCreatePackageMainSection"> + <element name="addProductsToPackage" type="button" selector="#package_block_1 button[data-action='package-add-items']"/> + <element name="addSelectedProductToPackage" type="button" selector="#package_block_1 button[data-action='package-save-items']"/> + <element name="save" type="button" selector="button[data-action='save-packages']"/> + <element name="saveButtonDisabled" type="button" selector="button[data-action='save-packages']._disabled"/> + </section> + <section name="AdminShipmentCreatePackageProductGridSection"> + <element name="concreteProductCheckbox" type="checkbox" selector="//td[contains(text(), '{{productName}}')]/parent::tr//input[contains(@class,'checkbox')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml index 0345c3f2949f4..3630de8978924 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -16,5 +16,6 @@ <element name="nameColumn" type="text" selector=".order-shipment-table .col-product .product-title"/> <element name="skuColumn" type="text" selector=".order-shipment-table .col-product .product-sku-block"/> <element name="itemQtyInvoiced" type="text" selector="(//*[@class='col-ordered-qty']//th[contains(text(), 'Invoiced')]/following-sibling::td)[{{var}}]" parameterized="true"/> + <element name="productQty" type="text" selector="td.col-qty"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentPaymentShippingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentPaymentShippingSection.xml index 48c7106c2d65e..eb94014d7a50c 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentPaymentShippingSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentPaymentShippingSection.xml @@ -17,7 +17,7 @@ <element name="AddTrackingNumber" type="button" selector="#tracking_numbers_table tfoot [data-ui-id='shipment-tracking-add-button']"/> <element name="Carrier" type="select" selector="#tracking_numbers_table tr:nth-of-type({{row}}) .col-carrier select" parameterized="true"/> <element name="Title" type="input" selector="#tracking_numbers_table tr:nth-of-type({{row}}) .col-title input" parameterized="true"/> - <element name="Number" type="input" selector="#tracking_numbers_table tr:nth-of-type({{row}} .col-number input)" parameterized="true"/> + <element name="Number" type="input" selector="#tracking_numbers_table tr:nth-of-type({{row}}) .col-number input" parameterized="true"/> <element name="Delete" type="button" selector="#tracking_numbers_table tr:nth-of-type({{row}} .col-delete button.action-delete)" parameterized="true"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml index f2f39d77d8d79..d76ba0493829e 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml @@ -12,5 +12,6 @@ <element name="CommentText" type="textarea" selector="#shipment_comment_text"/> <element name="AppendComments" type="checkbox" selector=".order-totals input#notify_customer"/> <element name="EmailCopy" type="checkbox" selector=".order-totals input#send_email"/> + <element name="createShippingLabel" type="checkbox" selector="input#create_shipping_label"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingInformationShippingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingInformationShippingSection.xml new file mode 100644 index 0000000000000..bbb61ed013a30 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingInformationShippingSection.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="AdminShipmentTrackingInformationShippingSection"> + <element name="shippingInfoTable" type="block" selector="#shipment_tracking_info"/> + <element name="shippingMethod" type="text" selector="#shipment_tracking_info .odd .col-carrier"/> + <element name="shippingMethodTitle" type="text" selector="#shipment_tracking_info .odd .col-title"/> + <element name="shippingNumber" type="text" selector="#shipment_tracking_info .odd .col-number"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingSection.xml new file mode 100644 index 0000000000000..52a5242f2d117 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingSection.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="AdminShipmentTrackingSection"> + <element name="trackingNumber" type="text" selector="#tracking-shipping-form #tracking_number"/> + <element name="trackingTitle" type="text" selector="#tracking-shipping-form #tracking_title"/> + <element name="addTrackingNumber" type="button" selector="#tracking-shipping-form button.save"/> + <element name="deleteTrackingNumber" type="button" selector="#tracking-shipping-form button.action-delete"/> + <element name="trackingInfoErrorElement" type="text" selector="#tracking-shipping-form #{{inputName}}-error" parameterized="true" /> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentsGridSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentsGridSection.xml new file mode 100644 index 0000000000000..84aed3052c736 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentsGridSection.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="AdminShipmentsGridSection"> + <element name="shipmentId" type="text" selector="//*[@id='sales_order_view_tabs_order_shipments_content']//tbody/tr/td[2]/div"/> + <element name="clearFilters" type="button" selector="button.action-tertiary.action-clear"/> + <element name="buttonFilters" type="button" selector=".data-grid-filters-action-wrap > button"/> + <element name="fieldShipment" type="input" selector="input[name='increment_id']"/> + <element name="applyFilter" type="button" selector="button[data-action='grid-filter-apply']"/> + <element name="rowShipments" type="text" selector="div.data-grid-cell-content"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml index a7ed0ab498bea..99c191a5225cc 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml @@ -11,5 +11,11 @@ <section name="AdminShippingMethodFlatRateSection"> <element name="carriersFlatRateTab" type="button" selector="#carriers_flatrate-head"/> <element name="carriersFlatRateActive" type="select" selector="#carriers_flatrate_active"/> + <element name="carriersEnableFlatRateActive" type="input" selector="#carriers_flatrate_active_inherit"/> + <element name="carriersFlatRateTitle" type="input" selector="#carriers_flatrate_title_inherit"/> + <element name="carriersFlatRateName" type="input" selector="#carriers_flatrate_name_inherit"/> + <element name="carriersFlatRateSpecificErrMsg" type="input" selector="#carriers_flatrate_specificerrmsg_inherit"/> + <element name="carriersFlatRateAllowSpecific" type="input" selector="#carriers_flatrate_sallowspecific_inherit"/> + <element name="carriersFlatRateSpecificCountry" type="input" selector="#carriers_flatrate_specificcountry"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFreeShippingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFreeShippingSection.xml new file mode 100644 index 0000000000000..bf8b5b9c33672 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFreeShippingSection.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="AdminShippingMethodFreeShippingSection"> + <element name="carriersFreeShippingSectionHead" type="button" selector="#carriers_freeshipping-head"/> + <element name="carriersFreeShippingActive" type="input" selector="#carriers_freeshipping_active_inherit"/> + <element name="carriersFreeShippingTitle" type="input" selector="#carriers_freeshipping_title_inherit"/> + <element name="carriersFreeShippingName" type="input" selector="#carriers_freeshipping_name_inherit"/> + <element name="carriersFreeShippingSpecificErrMsg" type="input" selector="#carriers_freeshipping_specificerrmsg_inherit"/> + <element name="carriersFreeShippingAllowSpecific" type="input" selector="#carriers_freeshipping_sallowspecific_inherit"/> + <element name="carriersFreeShippingSpecificCountry" type="input" selector="#carriers_freeshipping_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml index 3c570201c9970..944fc06047aa5 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml @@ -14,5 +14,13 @@ <element name="carriersTableRateActive" type="select" selector="#carriers_tablerate_active"/> <element name="condition" type="select" selector="#carriers_tablerate_condition_name"/> <element name="importFile" type="input" selector="#carriers_tablerate_import"/> + <element name="carriersTableRateTitle" type="input" selector="#carriers_tablerate_title_inherit"/> + <element name="carriersTableRateName" type="input" selector="#carriers_tablerate_name_inherit"/> + <element name="carriersTableRateConditionName" type="input" selector="#carriers_tablerate_condition_name_inherit"/> + <element name="carriersTableRateIncludeVirtualPrice" type="input" selector="#carriers_tablerate_include_virtual_price_inherit"/> + <element name="carriersTableRateHandlingType" type="input" selector="#carriers_tablerate_handling_type_inherit"/> + <element name="carriersTableRateSpecificErrMsg" type="input" selector="#carriers_tablerate_specificerrmsg_inherit"/> + <element name="carriersTableRateAllowSpecific" type="input" selector="#carriers_tablerate_sallowspecific_inherit"/> + <element name="carriersTableRateSpecificCountry" type="input" selector="#carriers_tablerate_specificcountry"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..0b7ddd0cfa781 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,84 @@ +<?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="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <annotations> + <features value="Configuration"/> + <stories value="Disable configuration inputs"/> + <title value="Check that all input fields disabled after executing CLI app:config:dump"/> + <description value="Check that all input fields disabled after executing CLI app:config:dump"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11158"/> + <useCaseId value="MAGETWO-96428"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Assert configuration are disabled in Flat Rate section--> + <comment userInput="Assert configuration are disabled in Flat Rate section" stepKey="commentSeeDisabledFlatRateConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateTab}}" dependentSelector="{{AdminShippingMethodFlatRateSection.carriersFlatRateActive}}" visible="false" stepKey="expandFlatRateTab"/> + <waitForElementVisible selector="{{AdminShippingMethodFlatRateSection.carriersEnableFlatRateActive}}" stepKey="waitForFlatRateTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersEnableFlatRateActive}}" userInput="disabled" stepKey="grabFlatRateActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateActiveDisabled" stepKey="assertFlatRateActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateTitle}}" userInput="disabled" stepKey="grabFlatRateTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateTitleDisabled" stepKey="assertFlatRateTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateName}}" userInput="disabled" stepKey="grabFlatRateNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateNameDisabled" stepKey="assertFlatRateNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateSpecificErrMsg}}" userInput="disabled" stepKey="grabFlatRateSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateSpecificErrMsgDisabled" stepKey="assertFlatRateSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateAllowSpecific}}" userInput="disabled" stepKey="grabFlatRateAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateAllowSpecificDisabled" stepKey="assertFlatRateAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateSpecificCountry}}" userInput="disabled" stepKey="grabFlatRateSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateSpecificCountryDisabled" stepKey="assertFlatRateSpecificCountryDisabled"/> + <!--Assert configuration are disabled in Free Shipping section--> + <comment userInput="Assert configuration are disabled in Free Shipping section" stepKey="commentSeeDisabledFreeShippingConfigs"/> + <conditionalClick selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingSectionHead}}" dependentSelector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingActive}}" visible="false" stepKey="expandFreeShippingTab"/> + <waitForElementVisible selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingActive}}" stepKey="waitForFreeShippingTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingActive}}" userInput="disabled" stepKey="grabFreeShippingActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingActiveDisabled" stepKey="assertFreeShippingActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingTitle}}" userInput="disabled" stepKey="grabFreeShippingTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingTitleDisabled" stepKey="assertFreeShippingTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingName}}" userInput="disabled" stepKey="grabFreeShippingNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingNameDisabled" stepKey="assertFreeShippingNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingSpecificErrMsg}}" userInput="disabled" stepKey="grabFreeShippingSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingSpecificErrMsgDisabled" stepKey="assertFreeShippingSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingAllowSpecific}}" userInput="disabled" stepKey="grabFreeShippingAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingAllowSpecificDisabled" stepKey="assertFreeShippingAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingSpecificCountry}}" userInput="disabled" stepKey="grabFreeShippingSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingSpecificCountryDisabled" stepKey="assertFreeShippingSpecificCountryDisabled"/> + <!--Assert configuration are disabled in Table Rates section--> + <comment userInput="Assert configuration are disabled in Table Rates section" stepKey="commentSeeDisabledTableRatesConfigs"/> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTableRateTab"/> + <waitForElementVisible selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="waitForTableRateTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" userInput="disabled" stepKey="grabTableRateActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateActiveDisabled" stepKey="assertTableRateActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTitle}}" userInput="disabled" stepKey="grabTableRateTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateTitleDisabled" stepKey="assertTableRateTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateName}}" userInput="disabled" stepKey="grabTableRateNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateNameDisabled" stepKey="assertTableRateNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" userInput="disabled" stepKey="grabTableRateConditionNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateConditionNameDisabled" stepKey="assertTableRateConditionNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateIncludeVirtualPrice}}" userInput="disabled" stepKey="grabTableRateIncludeVirtualPriceDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateIncludeVirtualPriceDisabled" stepKey="assertTableRateIncludeVirtualPriceDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateHandlingType}}" userInput="disabled" stepKey="grabTableRateHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateHandlingTypeDisabled" stepKey="assertTableRateHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateSpecificErrMsg}}" userInput="disabled" stepKey="grabTableRateSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateSpecificErrMsgDisabled" stepKey="assertTableRateSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateAllowSpecific}}" userInput="disabled" stepKey="grabTableRateAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateAllowSpecificDisabled" stepKey="assertTableRateAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateSpecificCountry}}" userInput="disabled" stepKey="grabTableRateSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateSpecificCountryDisabled" stepKey="assertTableRateSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml new file mode 100644 index 0000000000000..87058245c6014 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml @@ -0,0 +1,44 @@ +<?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="AdminCheckTheConfirmationPopupTest"> + <annotations> + <stories value="Admin confirmation modal should be in Magento style"/> + <title value="Admin confirmation modal should be in Magento style"/> + <description value="Testing the confirmation modal for removing the tracking number"/> + <severity value="CRITICAL"/> + <group value="shipping"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="CreateOrderActionGroup" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="createShipmentForOrder"/> + <actionGroup ref="FilterShipmentGridByOrderIdActionGroup" stepKey="filterForNewlyCreatedShipment"> + <argument name="orderId" value="$orderId"/> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="selectShipmentFromGrid"/> + <actionGroup ref="AdminAddTrackingNumberToShipmentActionGroup" stepKey="addTrackingNumber"> + <argument name="trackingNumber" value="123123"/> + </actionGroup> + <actionGroup ref="AdminDeleteTrackingNumberActionGroup" stepKey="deleteTrackingNumber"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml new file mode 100644 index 0000000000000..b2e3e2516a5c3 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml @@ -0,0 +1,107 @@ +<?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="AdminCreateOrderCustomStoreShippingMethodTableRatesTest"> + <annotations> + <features value="Shipping"/> + <stories value="Shipping method Table Rates"/> + <title value="Create order on second store with shipping method Table Rates"/> + <description value="Create order on second store with shipping method Table Rates"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6411"/> + <useCaseId value="MAGETWO-91702"/> + <group value="shipping"/> + </annotations> + <before> + <!--Create product and customer--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website, store group and store view--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + <!--Create customer associated to website--> + <actionGroup ref="AdminGoCreatedWebsitePageActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <grabFromCurrentUrl regex="~/website_id/(\d+)/~" stepKey="grabWebsiteIdFromURL"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"> + <field key="website_id">$grabWebsiteIdFromURL</field> + </createData> + <!--Enable Table Rate method and import csv file--> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="switchDefaultWebsite"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethodForDefaultWebsite"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigForDefaultWebsite"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="switchCustomWebsite"> + <argument name="website" value="customWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"> + <argument name="status" value="1"/> + </actionGroup> + <actionGroup ref="AdminImportFileTableRatesShippingMethodActionGroup" stepKey="importCSVFile"> + <argument name="file" value="usa_tablerates.csv"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Assign product to custom website--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="unassignWebsiteFromProductActionGroup" stepKey="unassignWebsiteInProduct"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectWebsiteInProduct"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Create order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToTheOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerInfo"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!--Choose Best Way shipping Method--> + <actionGroup ref="AdminOrderSelectShippingMethodActionGroup" stepKey="chooseBestWayMethod"> + <argument name="methodTitle" value="bestway"/> + <argument name="methodName" value="tablerate"/> + </actionGroup> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml new file mode 100644 index 0000000000000..30dc98c2f68ca --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml @@ -0,0 +1,76 @@ +<?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="AdminCreatePartialShipmentEntityTest"> + <annotations> + <stories value="Create Partial Shipment Entity"/> + <title value="Create Partial Shipment for Offline Payment Methods"/> + <description value="Admin Should be Able to Create Partial Shipments"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13331"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + </createData> + <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Free Shipping" --> + <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <magentoCLI command="cache:clean config" stepKey="flushCache"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- TEST BODY --> + <!-- Create Order --> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productQty" value="2"/> + </actionGroup> + <!-- Select Free shipping --> + <actionGroup ref="orderSelectFreeShipping" stepKey="selectFreeShippingOption"/> + <!--Click *Submit Order* button--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <!-- Create Partial Shipment --> + <actionGroup ref="AdminCreateShipmentFromOrderPage" stepKey="createNewShipment"> + <argument name="Qty" value="1"/> + <argument name="Title" value="Title"/> + <argument name="Number" value="199"/> + <argument name="Comment" value="comments for shipment"/> + </actionGroup> + <!-- Assert There is no "Ship Button" in Order Information --> + <actionGroup ref="AssertThereIsNoShipButtonActionGroup" stepKey="dontSeeShipButton"/> + <!-- Assert Created Shipment in Shipments Tab--> + <actionGroup ref="AdminAssertCreatedShipmentsInShipmentsTabActionGroup" stepKey="assertCreatedShipment"/> + <grabTextFrom selector="{{AdminShipmentsGridSection.shipmentId}}" stepKey="grabShipmentId"/> + <!-- Assert Shipment items --> + <actionGroup ref="AdminAssertShipmentItemsActionGroup" stepKey="assertShipmentItems"> + <argument name="product" value="$$createSimpleProduct.name$$"/> + <argument name="qty" value="1"/> + </actionGroup> + <!-- Assert Created Shipment in Shipments Grid--> + <actionGroup ref="AdminAssertShipmentInShipmentsGrid" stepKey="assertShipmentInGrid"> + <argument name="shipmentId" value="{$grabShipmentId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml new file mode 100644 index 0000000000000..3e6bf29b1bf54 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml @@ -0,0 +1,76 @@ +<?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="AdminCreateShipmentEntityWithTrackingNumberTest"> + <annotations> + <stories value="Shipment Entity With Tracking Number"/> + <title value="Create Shipment for Offline Payment Methods"/> + <description value="Admin Should be Able to Create Shipments"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14330"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + </createData> + <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Free Shipping" --> + <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <magentoCLI command="cache:clean config" stepKey="flushCache"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- TEST BODY --> + + <!-- Create Order --> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!-- Select Free shipping --> + <actionGroup ref="orderSelectFreeShipping" stepKey="selectFreeShippingOption"/> + <!--Click *Submit Order* button--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + <!-- Create Shipment --> + <actionGroup ref="AdminCreateShipmentFromOrderPage" stepKey="createNewShipment"> + <argument name="Title" value="Title"/> + <argument name="Number" value="199"/> + <argument name="Qty" value="1"/> + <argument name="Comment" value="comments for shipment"/> + </actionGroup> + <!-- Assert There is no "Ship Button" in Order Information --> + <actionGroup ref="AssertThereIsNoShipButtonActionGroup" stepKey="dontSeeShipButton"/> + <!-- Assert Created Shipment in Shipments Tab--> + <actionGroup ref="AdminAssertCreatedShipmentsInShipmentsTabActionGroup" stepKey="assertCreatedShipment"/> + <grabTextFrom selector="{{AdminShipmentsGridSection.shipmentId}}" stepKey="grabShipmentId"/> + <!-- Assert Shipment items --> + <actionGroup ref="AdminAssertShipmentItemsActionGroup" stepKey="assertShipmentItems"> + <argument name="product" value="$$createSimpleProduct.name$$"/> + <argument name="qty" value="1"/> + </actionGroup> + <!-- Assert Created Shipment in Shipments Grid--> + <actionGroup ref="AdminAssertShipmentInShipmentsGrid" stepKey="assertShipmentInGrid"> + <argument name="shipmentId" value="{$grabShipmentId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml new file mode 100644 index 0000000000000..ca4d731eb82b1 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml @@ -0,0 +1,52 @@ +<?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="AdminValidateShippingTrackingNumberTest"> + <annotations> + <stories value="Admin validate the shipping tracking number for an order"/> + <title value="Admin validate the shipping tracking number for an order"/> + <description value="Testing for a required tracking number when adding new shipping information"/> + <severity value="CRITICAL"/> + <group value="shipping"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="CreateOrderActionGroup" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="createShipmentForOrder"/> + <actionGroup ref="FilterShipmentGridByOrderIdActionGroup" stepKey="filterForNewlyCreatedShipment"> + <argument name="orderId" value="$orderId"/> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="selectShipmentFromGrid"/> + <actionGroup ref="AdminAddTrackingNumberToShipmentActionGroup" stepKey="addTrackingInformation"> + <argument name="trackingNumber" value=""/> + </actionGroup> + <actionGroup ref="AdminAssertTrackingValidationErrorActionGroup" stepKey="assertValidateTrackingNumber"> + <argument name="inputName" value="tracking_number"/> + </actionGroup> + <actionGroup ref="AdminAddTrackingNumberToShipmentActionGroup" stepKey="addTrackingNumber"> + <argument name="trackingNumber" value="123123"/> + </actionGroup> + <actionGroup ref="AdminAssertExistingTrackingNumberActionGroup" stepKey="checkAddedTrackingNumber"> + <argument name="trackingNumber" value="123123"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml new file mode 100644 index 0000000000000..bb29a4a28bcf6 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -0,0 +1,76 @@ +<?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="StorefrontDisplayTableRatesShippingMethodForAETest"> + <annotations> + <features value="Shipping"/> + <stories value="Table Rates"/> + <title value="Displaying of Table Rates for Armed Forces Europe (AE)"/> + <description value="Displaying of Table Rates for Armed Forces Europe (AE)"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6405"/> + <group value="shipping"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_ArmedForcesEurope" stepKey="createCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Rollback config--> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodSystemConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewToMainWebsite"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="disableTableRatesShippingMethod"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveSystemConfig"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Admin Configuration: enable Table Rates and import CSV file with the rates--> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="tablerates.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <!--Login as created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add the created product to the shopping cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!--Proceed to Checkout from the mini cart--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + <!--Shipping Method: select table rate--> + <actionGroup ref="AssertStoreFrontShippingMethodAvailableActionGroup" stepKey="assertShippingMethodAvailable"> + <argument name="shippingMethodName" value="Best Way"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodTableRate"> + <argument name="shippingMethodName" value="Best Way"/> + </actionGroup> + <!--Proceed to Review and Payments section--> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickToSaveShippingInfo"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <waitForPageLoad stepKey="waitForReviewAndPaymentsPageIsLoaded"/> + <!--Place order and assert the message of success--> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrderProductSuccessful"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Unit/Helper/DataTest.php b/app/code/Magento/Shipping/Test/Unit/Helper/DataTest.php new file mode 100644 index 0000000000000..b82e3537d26e2 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Unit/Helper/DataTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Shipping\Test\Unit\Helper; + +use PHPUnit\Framework\TestCase; +use Magento\Shipping\Helper\Data as HelperData; +use Magento\Framework\Url\DecoderInterface; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Data helper test + * + * Class \Magento\Shipping\Test\Unit\Helper\DataTest + */ +class DataTest extends TestCase +{ + /** + * @var HelperData + */ + private $helper; + + /** + * @var DecoderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlDecoderMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * Setup environment to test + */ + protected function setUp() + { + $this->contextMock = $this->createMock(Context::class); + $this->urlDecoderMock = $this->createMock(DecoderInterface::class); + $this->contextMock->expects($this->any())->method('getUrlDecoder') + ->willReturn($this->urlDecoderMock); + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->helper = $this->objectManagerHelper->getObject( + HelperData::class, + [ + 'context' => $this->contextMock + ] + ); + } + + /** + * test decodeTrackingHash() with data provider below + * + * @param string $hash + * @param string $urlDecodeResult + * @param array $expected + * @dataProvider decodeTrackingHashDataProvider + */ + public function testDecodeTrackingHash($hash, $urlDecodeResult, $expected) + { + $this->urlDecoderMock->expects($this->any())->method('decode') + ->with($hash) + ->willReturn($urlDecodeResult); + $this->assertEquals($expected, $this->helper->decodeTrackingHash($hash)); + } + + /** + * Dataset to test getData() + * + * @return array + */ + public function decodeTrackingHashDataProvider() + { + return [ + 'Test with hash key is allowed' => [ + strtr(base64_encode('order_id:1:protected_code'), '+/=', '-_,'), + 'order_id:1:protected_code', + [ + 'key' => 'order_id', + 'id' => 1, + 'hash' => 'protected_code' + ] + ], + 'Test with hash key is not allowed' => [ + strtr(base64_encode('invoice_id:1:protected_code'), '+/=', '-_,'), + 'invoice_id:1:protected_code', + [] + ] + ]; + } +} diff --git a/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php b/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php index 1df41aeba076b..e5723c38ac568 100644 --- a/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php @@ -3,12 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Test\Unit\Model; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type as ProductType; use Magento\CatalogInventory\Model\Stock\Item as StockItem; use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Shipping\Model\Carrier\AbstractCarrierInterface; use Magento\Shipping\Model\CarrierFactory; @@ -19,12 +21,14 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * @see Shipping + * Unit tests for \Magento\Shipping\Model\Shipping class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShippingTest extends \PHPUnit\Framework\TestCase { /** - * Test identification number of product + * Test identification number of product. * * @var int */ @@ -50,22 +54,34 @@ class ShippingTest extends \PHPUnit\Framework\TestCase */ private $carrier; + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + + /** + * @inheritdoc + */ protected function setUp() { $this->stockRegistry = $this->createMock(StockRegistry::class); $this->stockItemData = $this->createMock(StockItem::class); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); $this->shipping = (new ObjectManagerHelper($this))->getObject( Shipping::class, [ 'stockRegistry' => $this->stockRegistry, 'carrierFactory' => $this->getCarrierFactory(), + 'scopeConfig' => $this->scopeConfig, ] ); } /** + * Compose Packages For Carrier. * + * @return void */ public function testComposePackages() { @@ -125,14 +141,25 @@ function ($key) { /** * Active flag should be set before collecting carrier rates. + * + * @return void */ public function testCollectCarrierRatesSetActiveFlag() { + $carrierCode = 'carrier'; + $scopeStore = 'store'; + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with( + 'carriers/' . $carrierCode . '/active', + $scopeStore + ) + ->willReturn(true); $this->carrier->expects($this->atLeastOnce()) ->method('setActiveFlag') ->with('active'); - $this->shipping->collectCarrierRates('carrier', new RateRequest()); + $this->shipping->collectCarrierRates($carrierCode, new RateRequest()); } /** diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml index cd25cb919adb5..28322d9534926 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml @@ -31,7 +31,7 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : packaging.sendCreateLabelRequest(); }); packaging.setLabelCreatedCallback(function(response){ - setLocation("<?php $block->escapeJs($block->escapeUrl($block->getUrl( + setLocation("<?= $block->escapeJs($block->escapeUrl($block->getUrl( 'sales/order/view', ['order_id' => $block->getShipment()->getOrderId()] ))); ?>"); diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml index 67587f19774c4..a013abfd65f87 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml @@ -9,84 +9,102 @@ ?> <?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View */ ?> <div class="admin__control-table-wrapper"> - <table class="data-table admin__control-table" id="shipment_tracking_info"> - <thead> - <tr class="headings"> - <th class="col-carrier"><?= $block->escapeHtml(__('Carrier')) ?></th> - <th class="col-title"><?= $block->escapeHtml(__('Title')) ?></th> - <th class="col-number"><?= $block->escapeHtml(__('Number')) ?></th> - <th class="col-delete last"><?= $block->escapeHtml(__('Action')) ?></th> - </tr> - </thead> - <tfoot> - <tr> - <td class="col-carrier"> - <select name="carrier" - class="select admin__control-select" - onchange="selectCarrier(this)"> - <?php foreach ($block->getCarriers() as $_code => $_name) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> - <?php endforeach; ?> - </select> - </td> - <td class="col-title"> - <input class="input-text admin__control-text" - type="text" - id="tracking_title" - name="title" - value="" /> - </td> - <td class="col-number"> - <input class="input-text admin__control-text" - type="text" - id="tracking_number" - name="number" - value="" /> - </td> - <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> - </tr> - </tfoot> - <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> - <tbody> - <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> - <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> - <td class="col-carrier"> - <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> - </td> - <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> - <td class="col-number"> - <?php if ($_track->isCustom()) : ?> - <?= $block->escapeHtml($_track->getNumber()) ?> - <?php else : ?> - <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> - <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> - <?php endif; ?> - </td> - <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> - </tr> - <?php endforeach; ?> - </tbody> - <?php endif; ?> - </table> + <form id="tracking-shipping-form" data-mage-init='{"validation": {}}'> + <table class="data-table admin__control-table" id="shipment_tracking_info"> + <thead> + <tr class="headings"> + <th class="col-carrier"><?= $block->escapeHtml(__('Carrier')) ?></th> + <th class="col-title"><?= $block->escapeHtml(__('Title')) ?></th> + <th class="col-number"><?= $block->escapeHtml(__('Number')) ?></th> + <th class="col-delete last"><?= $block->escapeHtml(__('Action')) ?></th> + </tr> + </thead> + <tfoot> + <tr> + <td class="col-carrier"> + <select name="carrier" + class="select admin__control-select" + onchange="selectCarrier(this)"> + <?php foreach ($block->getCarriers() as $_code => $_name) : ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> + <?php endforeach; ?> + </select> + </td> + <td class="col-title"> + <input class="input-text admin__control-text" + type="text" + id="tracking_title" + name="title" + value="" /> + </td> + <td class="col-number"> + <input class="input-text admin__control-text required-entry" + type="text" + id="tracking_number" + name="number" + value="" /> + </td> + <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> + </tr> + </tfoot> + <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> + <tbody> + <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> + <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> + <td class="col-carrier"> + <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> + </td> + <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> + <td class="col-number"> + <?php if ($_track->isCustom()) : ?> + <?= $block->escapeHtml($_track->getNumber()) ?> + <?php else : ?> + <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> + <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> + <?php endif; ?> + </td> + <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> + </tr> + <?php endforeach; ?> + </tbody> + <?php endif; ?> + </table> + </form> </div> <script> -require(['prototype'], function(){ - +require(['prototype', 'jquery', 'Magento_Ui/js/modal/confirm'], function(prototype, $j, confirm) { //<![CDATA[ function selectCarrier(elem) { var option = elem.options[elem.selectedIndex]; $('tracking_title').value = option.value && option.value != 'custom' ? option.text : ''; } -function deleteTrackingNumber(url) { - if (confirm('<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>')) { - submitAndReloadArea($('shipment_tracking_info').parentNode, url) +function saveTrackingInfo(node, url) { + var form = $j('#tracking-shipping-form'); + + if (form.validation() && form.validation('isValid')) { + submitAndReloadArea(node, url); } } +function deleteTrackingNumber(url) { + confirm({ + content: '<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>', + actions: { + /** + * Confirm action. + */ + confirm: function () { + submitAndReloadArea($('shipment_tracking_info').parentNode, url); + } + } + }); +} + window.selectCarrier = selectCarrier; window.deleteTrackingNumber = deleteTrackingNumber; +window.saveTrackingInfo = saveTrackingInfo; //]]> }); diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml index f105562151082..44fe4b9ccd353 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml @@ -85,7 +85,7 @@ $order = $block->getShipment()->getOrder(); window.packaging.sendCreateLabelRequest(); }); window.packaging.setLabelCreatedCallback(function () { - setLocation("<?php $block->escapeUrl($block->getUrl('adminhtml/order_shipment/view', ['shipment_id' => $block->getShipment()->getId()])); ?>"); + setLocation("<?= $block->escapeUrl($block->getUrl('adminhtml/order_shipment/view', ['shipment_id' => $block->getShipment()->getId()])); ?>"); }); }; diff --git a/app/code/Magento/Shipping/view/frontend/templates/items.phtml b/app/code/Magento/Shipping/view/frontend/templates/items.phtml index f0f1423ed47a2..177628c6b2015 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/items.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/items.phtml @@ -15,8 +15,9 @@ <?= $block->getChildHtml('track-all-link') ?> <?php endif; ?> <a href="<?= $block->escapeUrl($block->getPrintAllShipmentsUrl($_order)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print All Shipments')) ?></span> </a> </div> @@ -24,8 +25,9 @@ <div class="order-title"> <strong><?= $block->escapeHtml(__('Shipment #')) ?><?= $block->escapeHtml($_shipment->getIncrementId()) ?></strong> <a href="<?= $block->escapeUrl($block->getPrintShipmentUrl($_shipment)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Shipment')) ?></span> </a> <a href="#" diff --git a/app/code/Magento/Signifyd/Model/CaseSearchResults.php b/app/code/Magento/Signifyd/Model/CaseSearchResults.php new file mode 100644 index 0000000000000..ff1ab8839f6cd --- /dev/null +++ b/app/code/Magento/Signifyd/Model/CaseSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Signifyd\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Signifyd\Api\Data\CaseSearchResultsInterface; + +/** + * Service Data Object with Case entities search results. + */ +class CaseSearchResults extends SearchResults implements CaseSearchResultsInterface +{ +} diff --git a/app/code/Magento/Signifyd/Test/Mftf/Page/AdminFraudProtectionPage.xml b/app/code/Magento/Signifyd/Test/Mftf/Page/AdminFraudProtectionPage.xml new file mode 100644 index 0000000000000..07b58b8594843 --- /dev/null +++ b/app/code/Magento/Signifyd/Test/Mftf/Page/AdminFraudProtectionPage.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="AdminFraudProtectionPage" url="admin/system_config/edit/section/fraud_protection/" area="admin" module="Magento_Signifyd"> + <section name="AdminSignifydConfigurationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Signifyd/Test/Mftf/Section/AdminSignifydConfigurationSection.xml b/app/code/Magento/Signifyd/Test/Mftf/Section/AdminSignifydConfigurationSection.xml new file mode 100644 index 0000000000000..618e9d520dd87 --- /dev/null +++ b/app/code/Magento/Signifyd/Test/Mftf/Section/AdminSignifydConfigurationSection.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="AdminSignifydConfigurationSection"> + <element name="head" type="button" selector="#fraud_protection_signifyd_config-head"/> + <element name="enabled" type="input" selector="#fraud_protection_signifyd_config_active"/> + <element name="url" type="text" selector="#fraud_protection_signifyd_config_api_url"/> + </section> +</sections> diff --git a/app/code/Magento/Signifyd/Test/Mftf/Test/AdminSignifydConfigDependentOnActiveFieldTest.xml b/app/code/Magento/Signifyd/Test/Mftf/Test/AdminSignifydConfigDependentOnActiveFieldTest.xml new file mode 100644 index 0000000000000..dcae0c4091ba6 --- /dev/null +++ b/app/code/Magento/Signifyd/Test/Mftf/Test/AdminSignifydConfigDependentOnActiveFieldTest.xml @@ -0,0 +1,33 @@ +<?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="AdminSignifydConfigDependentOnActiveFieldTest"> + <annotations> + <features value="Signifyd"/> + <title value="Signifyd config dependent on active field" /> + <description value="Signifyd system configs dependent by Enable this Solution field."/> + <severity value="MINOR"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set fraud_protection/signifyd/active 1" stepKey="enableSignifyd"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="config:set fraud_protection/signifyd/active 0" stepKey="disableSignifyd"/> + </after> + + <amOnPage url="{{AdminFraudProtectionPage.url}}" stepKey="openFraudProtectionPagePage" /> + <conditionalClick dependentSelector="{{AdminSignifydConfigurationSection.enabled}}" visible="false" selector="{{AdminSignifydConfigurationSection.head}}" stepKey="openCollapsibleBlock"/> + <seeInField selector="{{AdminSignifydConfigurationSection.url}}" userInput="https://api.signifyd.com/v2/" stepKey="seeApiUrlField"/> + <selectOption selector="{{AdminSignifydConfigurationSection.enabled}}" userInput="0" stepKey="disableSignifydOption"/> + <dontSeeElement selector="{{AdminSignifydConfigurationSection.url}}" stepKey="dontSeeApiUrlField"/> + </test> +</tests> diff --git a/app/code/Magento/Signifyd/etc/adminhtml/system.xml b/app/code/Magento/Signifyd/etc/adminhtml/system.xml index 2dd75d2d91e5b..272ce78aec2e5 100644 --- a/app/code/Magento/Signifyd/etc/adminhtml/system.xml +++ b/app/code/Magento/Signifyd/etc/adminhtml/system.xml @@ -38,25 +38,37 @@ </field> <field id="api_key" translate="label" type="obscure" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0"> <label>API Key</label> - <comment><![CDATA[Your API key can be found on the <a href="http://signifyd.com/settings" target="_blank">settings page</a> in the Signifyd console]]></comment> + <comment><![CDATA[Your API key can be found on the <a href="http://signifyd.com/settings" target="_blank">settings page</a> in the Signifyd console.]]></comment> <config_path>fraud_protection/signifyd/api_key</config_path> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <depends> + <field id="active">1</field> + </depends> </field> <field id="api_url" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> <label>API URL</label> <config_path>fraud_protection/signifyd/api_url</config_path> <comment>Don’t change unless asked to do so.</comment> + <depends> + <field id="active">1</field> + </depends> </field> <field id="debug" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Debug</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>fraud_protection/signifyd/debug</config_path> + <depends> + <field id="active">1</field> + </depends> </field> <field id="webhook_url" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Webhook URL</label> <comment><![CDATA[Your webhook URL will be used to <a href="https://app.signifyd.com/settings/notifications" target="_blank">configure</a> a guarantee completed webhook in Signifyd. Webhooks are used to sync Signifyd`s guarantee decisions back to Magento.]]></comment> <attribute type="handler_url">signifyd/webhooks/handler</attribute> <frontend_model>Magento\Signifyd\Block\Adminhtml\System\Config\Field\WebhookUrl</frontend_model> + <depends> + <field id="active">1</field> + </depends> </field> </group> </group> diff --git a/app/code/Magento/Signifyd/etc/di.xml b/app/code/Magento/Signifyd/etc/di.xml index c586019ca3d12..e82e8f84b3584 100644 --- a/app/code/Magento/Signifyd/etc/di.xml +++ b/app/code/Magento/Signifyd/etc/di.xml @@ -9,7 +9,7 @@ <preference for="Magento\Signifyd\Api\Data\CaseInterface" type="Magento\Signifyd\Model\CaseEntity" /> <preference for="Magento\Signifyd\Api\CaseRepositoryInterface" type="Magento\Signifyd\Model\CaseRepository" /> <preference for="Magento\Signifyd\Api\CaseManagementInterface" type="Magento\Signifyd\Model\CaseManagement" /> - <preference for="Magento\Signifyd\Api\Data\CaseSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Signifyd\Api\Data\CaseSearchResultsInterface" type="Magento\Signifyd\Model\CaseSearchResults" /> <preference for="Magento\Signifyd\Api\CaseCreationServiceInterface" type="Magento\Signifyd\Model\CaseServices\CreationService" /> <preference for="Magento\Signifyd\Api\GuaranteeCreationServiceInterface" type="Magento\Signifyd\Model\Guarantee\CreationService" /> <preference for="Magento\Signifyd\Api\GuaranteeCancelingServiceInterface" type="Magento\Signifyd\Model\Guarantee\CancelingService" /> diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php index 14771e7f03a3b..117f73311b644 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php @@ -6,25 +6,30 @@ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Block\Template; +use Magento\Backend\Model\Session; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Registry; +use Magento\Sitemap\Controller\Adminhtml\Sitemap; /** * Controller class Edit. Responsible for rendering of a sitemap edit page */ -class Edit extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpGetActionInterface +class Edit extends Sitemap implements HttpGetActionInterface { /** * Core registry * - * @var \Magento\Framework\Registry + * @var Registry */ - protected $_coreRegistry = null; + protected $_coreRegistry; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry + * @param Context $context + * @param Registry $coreRegistry */ - public function __construct(\Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry) + public function __construct(Context $context, Registry $coreRegistry) { $this->_coreRegistry = $coreRegistry; parent::__construct($context); @@ -53,7 +58,7 @@ public function execute() } // 3. Set entered data if was error when we do save - $data = $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getFormData(true); + $data = $this->_objectManager->get(Session::class)->getFormData(true); if (!empty($data)) { $model->setData($data); } @@ -67,6 +72,8 @@ public function execute() $id ? __('Edit Sitemap') : __('New Sitemap') )->_addContent( $this->_view->getLayout()->createBlock(\Magento\Sitemap\Block\Adminhtml\Edit::class) + )->_addJs( + $this->_view->getLayout()->createBlock(Template::class)->setTemplate('Magento_Sitemap::js.phtml') ); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Site Map')); $this->_view->getPage()->getConfig()->getTitle()->prepend( diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php index 8eeeb5bf6bc12..5cfc7349888f3 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php @@ -49,7 +49,13 @@ public function execute() // if sitemap record exists if ($sitemap->getId()) { try { + $this->appEmulation->startEnvironmentEmulation( + $sitemap->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $sitemap->generateXml(); + $this->appEmulation->stopEnvironmentEmulation(); $this->messageManager->addSuccessMessage( __('The sitemap "%1" has been generated.', $sitemap->getSitemapFilename()) ); diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 0d69634ccfa5e..2baa6ff2c71a7 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -10,6 +10,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; use Magento\Framework\UrlInterface; use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Model\ItemProvider\ItemProviderInterface; @@ -191,6 +192,16 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento */ private $lastModMinTsVal; + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var DocumentRoot + */ + private $documentRoot; + /** * Initialize dependencies. * @@ -238,8 +249,9 @@ public function __construct( ) { $this->_escaper = $escaper; $this->_sitemapData = $sitemapData; - $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); - $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); + $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); + $this->filesystem = $filesystem; + $this->_directory = $filesystem->getDirectoryWrite($this->documentRoot->getPath()); $this->_categoryFactory = $categoryFactory; $this->_productFactory = $productFactory; $this->_cmsFactory = $cmsFactory; @@ -727,8 +739,8 @@ protected function _getFormattedLastmodDate($date) */ protected function _getDocumentRoot() { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return realpath($this->_request->getServer('DOCUMENT_ROOT')); + return $this->filesystem->getDirectoryRead($this->documentRoot->getPath()) + ->getAbsolutePath(); } /** diff --git a/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteDeleteByNameActionGroup.xml b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteDeleteByNameActionGroup.xml new file mode 100644 index 0000000000000..16bf43da2e690 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteDeleteByNameActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMarketingSiteDeleteByNameActionGroup"> + <annotations> + <description>Go to the Site map page. Delete a site map based on the provided Name.</description> + </annotations> + <arguments> + <argument name="filename" type="string"/> + </arguments> + + <amOnPage url="{{AdminMarketingSiteMapGridPage.url}}" stepKey="amOnSiteMapGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{AdminMarketingSiteMapGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField selector="{{AdminMarketingSiteMapGridSection.fileNameTextField}}" userInput="{{filename}}" stepKey="fillFileNameField"/> + <click selector="{{AdminMarketingSiteMapGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{filename}}" selector="{{AdminMarketingSiteMapGridSection.firstSearchResult}}" stepKey="verifyThatCorrectStoreGroupFound"/> + <click selector="{{AdminMarketingSiteMapGridSection.firstSearchResult}}" stepKey="clickEditExistingRow"/> + <waitForPageLoad stepKey="waitForSiteMapToLoad"/> + <click selector="{{AdminMarketingSiteMapEditActionSection.delete}}" stepKey="deleteSiteMap"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteMapFillFormActionGroup.xml b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteMapFillFormActionGroup.xml new file mode 100644 index 0000000000000..06e992736bf06 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteMapFillFormActionGroup.xml @@ -0,0 +1,23 @@ +<?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="AdminMarketingSiteMapFillFormActionGroup"> + <annotations> + <description>Fill data to Site map form</description> + </annotations> + <arguments> + <argument name="sitemap" type="entity" defaultValue="DefaultSiteMap"/> + </arguments> + <fillField selector="{{AdminMarketingSiteMapEditActionSection.filename}}" userInput="{{sitemap.filename}}" stepKey="fillFilename"/> + <fillField selector="{{AdminMarketingSiteMapEditActionSection.path}}" userInput="{{sitemap.path}}" stepKey="fillPath"/> + <click selector="{{AdminMarketingSiteMapEditActionSection.save}}" stepKey="saveSiteMap"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteMapNavigateNewActionGroup.xml b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteMapNavigateNewActionGroup.xml new file mode 100644 index 0000000000000..78cfeab66f1c4 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AdminMarketingSiteMapNavigateNewActionGroup.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="AdminMarketingSiteMapNavigateNewActionGroup"> + <annotations> + <description>Navigate to New Site Map</description> + </annotations> + <amOnPage url="{{AdminMarketingSiteMapNewPage.url}}" stepKey="openNewSiteMapPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AssertSiteMapCreateSuccessActionGroup.xml b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AssertSiteMapCreateSuccessActionGroup.xml new file mode 100644 index 0000000000000..77f26063e8034 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AssertSiteMapCreateSuccessActionGroup.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="AssertSiteMapCreateSuccessActionGroup"> + <annotations> + <description>Validate the success message after creating site map.</description> + </annotations> + + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the sitemap." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AssertSiteMapDeleteSuccessActionGroup.xml b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AssertSiteMapDeleteSuccessActionGroup.xml new file mode 100644 index 0000000000000..15df8aa2f25f1 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/ActionGroup/AssertSiteMapDeleteSuccessActionGroup.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="AssertSiteMapDeleteSuccessActionGroup"> + <annotations> + <description>Validate the success message after delete site map.</description> + </annotations> + + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the sitemap." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Data/SitemapData.xml b/app/code/Magento/Sitemap/Test/Mftf/Data/SitemapData.xml new file mode 100644 index 0000000000000..0b5d5d3dcdefe --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Data/SitemapData.xml @@ -0,0 +1,15 @@ +<?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="DefaultSiteMap"> + <data key="filename">sitemap.xml</data> + <data key="path">/</data> + </entity> +</entities> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Page/AdminMarketingSiteMapGridPage.xml b/app/code/Magento/Sitemap/Test/Mftf/Page/AdminMarketingSiteMapGridPage.xml new file mode 100644 index 0000000000000..b15a16bf134ad --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Page/AdminMarketingSiteMapGridPage.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="AdminMarketingSiteMapGridPage" url="admin/sitemap/" area="admin" module="Sitemap"> + <section name="AdminMarketingSiteMapGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Page/AdminMarketingSiteMapNewPage.xml b/app/code/Magento/Sitemap/Test/Mftf/Page/AdminMarketingSiteMapNewPage.xml new file mode 100644 index 0000000000000..5450ece5bb3c2 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Page/AdminMarketingSiteMapNewPage.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="AdminMarketingSiteMapNewPage" url="admin/sitemap/new/" area="admin" module="Sitemap"> + <section name="AdminMarketingSiteMapEditActionSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Section/AdminMarketingSiteMapEditActionSection.xml b/app/code/Magento/Sitemap/Test/Mftf/Section/AdminMarketingSiteMapEditActionSection.xml new file mode 100644 index 0000000000000..841071350526a --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Section/AdminMarketingSiteMapEditActionSection.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="AdminMarketingSiteMapEditActionSection"> + <element name="save" type="button" selector="#save" timeout="10"/> + <element name="delete" type="button" selector="#delete" timeout="10"/> + <element name="saveAndGenerate" type="button" selector="#generate" timeout="10"/> + <element name="reset" type="button" selector="#reset"/> + <element name="back" type="button" selector="#back"/> + <element name="filename" type="input" selector="input[name='sitemap_filename']"/> + <element name="path" type="input" selector="input[name='sitemap_path']"/> + </section> +</sections> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Section/AdminMarketingSiteMapGridSection.xml b/app/code/Magento/Sitemap/Test/Mftf/Section/AdminMarketingSiteMapGridSection.xml new file mode 100644 index 0000000000000..50c96ae6748ce --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Section/AdminMarketingSiteMapGridSection.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="AdminMarketingSiteMapGridSection"> + <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> + <element name="searchButton" type="button" selector=".admin__filter-actions [title='Search']"/> + <element name="firstSearchResult" type="text" selector="#sitemapGrid_table>tbody>tr:nth-child(1)"/> + <element name="fileNameTextField" type="input" selector="#sitemapGrid_filter_sitemap_filename" timeout="90"/> + </section> +</sections> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapCreateNewTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapCreateNewTest.xml new file mode 100644 index 0000000000000..57d8f8c75d23d --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapCreateNewTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingSiteMapCreateNewTest"> + <annotations> + <features value="Sitemap"/> + <stories value="Create Site Map"/> + <title value="Create New Site Map with valid data"/> + <description value="Create New Site Map with valid data"/> + <severity value="CRITICAL"/> + <group value="sitemap"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteSiteMap"> + <argument name="filename" value="{{DefaultSiteMap.filename}}" /> + </actionGroup> + <actionGroup ref="AssertSiteMapDeleteSuccessActionGroup" stepKey="assertDeleteSuccessMessage"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminMarketingSiteMapNavigateNewActionGroup" stepKey="navigateNewSiteMap"/> + <actionGroup ref="AdminMarketingSiteMapFillFormActionGroup" stepKey="fillSiteMapForm"> + <argument name="sitemap" value="DefaultSiteMap" /> + </actionGroup> + <actionGroup ref="AssertSiteMapCreateSuccessActionGroup" stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Sitemap/etc/db_schema.xml b/app/code/Magento/Sitemap/etc/db_schema.xml index b3c028b626b73..adf1f11124f52 100644 --- a/app/code/Magento/Sitemap/etc/db_schema.xml +++ b/app/code/Magento/Sitemap/etc/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="sitemap" resource="default" engine="innodb" comment="XML Sitemap"> <column xsi:type="int" name="sitemap_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Sitemap Id"/> + comment="Sitemap ID"/> <column xsi:type="varchar" name="sitemap_type" nullable="true" length="32" comment="Sitemap Type"/> <column xsi:type="varchar" name="sitemap_filename" nullable="true" length="32" comment="Sitemap Filename"/> <column xsi:type="varchar" name="sitemap_path" nullable="true" length="255" comment="Sitemap Path"/> <column xsi:type="timestamp" name="sitemap_time" on_update="false" nullable="true" comment="Sitemap Time"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store id"/> + default="0" comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="sitemap_id"/> </constraint> diff --git a/app/code/Magento/Sitemap/view/adminhtml/templates/js.phtml b/app/code/Magento/Sitemap/view/adminhtml/templates/js.phtml new file mode 100644 index 0000000000000..4e7ed34ed4a7e --- /dev/null +++ b/app/code/Magento/Sitemap/view/adminhtml/templates/js.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> + +<script type="text/x-magento-init"> + { + "#edit_form": { + "Magento_Sitemap/js/form-submit-loader" : {} + } + } +</script> diff --git a/app/code/Magento/Sitemap/view/adminhtml/web/js/form-submit-loader.js b/app/code/Magento/Sitemap/view/adminhtml/web/js/form-submit-loader.js new file mode 100644 index 0000000000000..6b7ba6be65b22 --- /dev/null +++ b/app/code/Magento/Sitemap/view/adminhtml/web/js/form-submit-loader.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (data, element) { + + $(element).on('save', function () { + if ($(this).valid()) { + $('body').trigger('processStart'); + } + }); + }; +}); diff --git a/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php b/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php index 77ccce5d23bde..f732871114061 100644 --- a/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php +++ b/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php @@ -44,6 +44,7 @@ public function __construct( /** * Return whole scopes config data from db. + * * Ignore $path argument due to config source must return all config data * * @param string $path @@ -64,6 +65,8 @@ public function get($path = '') } /** + * Retrieve default connection + * * @return AdapterInterface */ private function getConnection() @@ -83,12 +86,17 @@ private function getConnection() */ private function getEntities($table, $keyField) { - $entities = $this->getConnection()->fetchAll( - $this->getConnection()->select()->from($this->resourceConnection->getTableName($table)) - ); $data = []; - foreach ($entities as $entity) { - $data[$entity[$keyField]] = $entity; + $tableName = $this->resourceConnection->getTableName($table); + // Check if db table exists before fetch data + if ($this->resourceConnection->getConnection()->isTableExists($tableName)) { + $entities = $this->getConnection()->fetchAll( + $this->getConnection()->select()->from($tableName) + ); + + foreach ($entities as $entity) { + $data[$entity[$keyField]] = $entity; + } } return $data; diff --git a/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php b/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php index 5df50581792ce..de2da54423822 100644 --- a/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php +++ b/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php @@ -5,6 +5,9 @@ */ namespace Magento\Store\App\FrontController\Plugin; +/** + * Class RequestPreprocessor + */ class RequestPreprocessor { /** @@ -52,6 +55,7 @@ public function __construct( /** * Auto-redirect to base url (without SID) if the requested url doesn't match it. + * * By default this feature is enabled in configuration. * * @param \Magento\Framework\App\FrontController $subject @@ -72,10 +76,11 @@ public function aroundDispatch( $this->_storeManager->getStore()->isCurrentlySecure() ); if ($baseUrl) { + // phpcs:disable Magento2.Functions.DiscouragedFunction $uri = parse_url($baseUrl); if (!$this->getBaseUrlChecker()->execute($uri, $request)) { $redirectUrl = $this->_url->getRedirectUrl( - $this->_url->getUrl(ltrim($request->getPathInfo(), '/'), ['_nosid' => true]) + $this->_url->getDirectUrl(ltrim($request->getPathInfo(), '/'), ['_nosid' => true]) ); $redirectCode = (int)$this->_scopeConfig->getValue( 'web/url/redirect_to_base', diff --git a/app/code/Magento/Store/Controller/Store/SwitchRequest.php b/app/code/Magento/Store/Controller/Store/SwitchRequest.php new file mode 100644 index 0000000000000..9ce151bdab094 --- /dev/null +++ b/app/code/Magento/Store/Controller/Store/SwitchRequest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Controller\Store; + +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Store\Model\StoreSwitcher\HashGenerator; +use Magento\Customer\Api\CustomerRepositoryInterface; +use \Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Url\DecoderInterface; +use \Magento\Framework\App\ActionInterface; +use Magento\Store\Model\StoreSwitcher\HashGenerator\HashData; + +/** + * Builds correct url to target store and performs redirect. + */ +class SwitchRequest extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface +{ + + /** + * @var customerSession + */ + private $customerSession; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var HashGenerator + */ + private $hashGenerator; + + /** + * @var DecoderInterface + */ + private $urlDecoder; + + /** + * @param Context $context + * @param CustomerSession $session + * @param CustomerRepositoryInterface $customerRepository + * @param HashGenerator $hashGenerator + * @param DecoderInterface $urlDecoder + */ + public function __construct( + Context $context, + CustomerSession $session, + CustomerRepositoryInterface $customerRepository, + HashGenerator $hashGenerator, + DecoderInterface $urlDecoder + ) { + parent::__construct($context); + $this->customerSession = $session; + $this->customerRepository = $customerRepository; + $this->hashGenerator = $hashGenerator; + $this->urlDecoder = $urlDecoder; + } + + /** + * Execute action + * + * @return void + */ + public function execute() + { + $fromStoreCode = (string)$this->_request->getParam('___from_store'); + $customerId = (int)$this->_request->getParam('customer_id'); + $timeStamp = (string)$this->_request->getParam('time_stamp'); + $signature = (string)$this->_request->getParam('signature'); + $error = null; + $encodedUrl = (string)$this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED); + $targetUrl = $this->urlDecoder->decode($encodedUrl); + + $data = new HashData( + [ + "customer_id" => $customerId, + "time_stamp" => $timeStamp, + "___from_store" => $fromStoreCode + ] + ); + + if ($targetUrl && $this->hashGenerator->validateHash($signature, $data)) { + try { + $customer = $this->customerRepository->getById($customerId); + if (!$this->customerSession->isLoggedIn()) { + $this->customerSession->setCustomerDataAsLoggedIn($customer); + } + $this->getResponse()->setRedirect($targetUrl); + } catch (NoSuchEntityException $e) { + $error = __('The requested customer does not exist.'); + } catch (LocalizedException $e) { + $error = __('There was an error retrieving the customer record.'); + } + } else { + $error = __('The requested store cannot be found. Please check the request and try again.'); + } + + if ($error !== null) { + $this->messageManager->addErrorMessage($error); + } + } +} diff --git a/app/code/Magento/Store/Model/Config/Placeholder.php b/app/code/Magento/Store/Model/Config/Placeholder.php index ca5d869d347a4..be84c7f444c44 100644 --- a/app/code/Magento/Store/Model/Config/Placeholder.php +++ b/app/code/Magento/Store/Model/Config/Placeholder.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Store\Model\Config; /** @@ -32,8 +33,8 @@ class Placeholder */ public function __construct(\Magento\Framework\App\RequestInterface $request, $urlPaths, $urlPlaceholder) { - $this->request = $request; - $this->urlPaths = $urlPaths; + $this->request = $request; + $this->urlPaths = $urlPaths; $this->urlPlaceholder = $urlPlaceholder; } @@ -45,15 +46,46 @@ public function __construct(\Magento\Framework\App\RequestInterface $request, $u */ public function process(array $data = []) { - foreach (array_keys($data) as $key) { - $this->_processData($data, $key); + // check provided arguments + if (empty($data)) { + return []; + } + + // initialize $pointer, $parents and $level variable + reset($data); + $pointer = &$data; + $parents = []; + $level = 0; + + while ($level >= 0) { + $current = &$pointer[key($pointer)]; + if (is_array($current)) { + reset($current); + $parents[$level] = &$pointer; + $pointer = &$current; + $level++; + } else { + $current = $this->_processPlaceholders($current, $data); + + // move pointer of last queue layer to next element + // or remove layer if all path elements were processed + while ($level >= 0 && next($pointer) === false) { + $level--; + // removal of last element of $parents is skipped here for better performance + // on next iteration that element will be overridden + $pointer = &$parents[$level]; + } + } } + return $data; } /** * Process array data recursively * + * @deprecated This method isn't used in process() implementation anymore + * * @param array &$data * @param string $path * @return void @@ -90,7 +122,7 @@ protected function _processPlaceholders($value, $data) if ($url) { $value = str_replace('{{' . $placeholder . '}}', $url, $value); - } elseif (strpos($value, (string) $this->urlPlaceholder) !== false) { + } elseif (strpos($value, (string)$this->urlPlaceholder) !== false) { $distroBaseUrl = $this->request->getDistroBaseUrl(); $value = str_replace($this->urlPlaceholder, $distroBaseUrl, $value); @@ -113,10 +145,9 @@ protected function _getPlaceholder($value) { if (is_string($value) && preg_match('/{{(.*)}}.*/', $value, $matches)) { $placeholder = $matches[1]; - if ($placeholder == 'unsecure_base_url' || $placeholder == 'secure_base_url' || strpos( - $value, - (string) $this->urlPlaceholder - ) !== false + if ($placeholder == 'unsecure_base_url' || + $placeholder == 'secure_base_url' || + strpos($value, (string)$this->urlPlaceholder) !== false ) { return $placeholder; } @@ -147,6 +178,8 @@ protected function _getValue($path, array $data) /** * Set array value by path * + * @deprecated This method isn't used in process() implementation anymore + * * @param array &$container * @param string $path * @param string $value @@ -154,13 +187,13 @@ protected function _getValue($path, array $data) */ protected function _setValue(array &$container, $path, $value) { - $segments = explode('/', $path); - $currentPointer = & $container; + $segments = explode('/', $path); + $currentPointer = &$container; foreach ($segments as $segment) { if (!isset($currentPointer[$segment])) { $currentPointer[$segment] = []; } - $currentPointer = & $currentPointer[$segment]; + $currentPointer = &$currentPointer[$segment]; } $currentPointer = $value; } diff --git a/app/code/Magento/Store/Model/ResourceModel/Store.php b/app/code/Magento/Store/Model/ResourceModel/Store.php index 88d7b5d8216fe..7a2821987f9bf 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Store.php +++ b/app/code/Magento/Store/Model/ResourceModel/Store.php @@ -166,11 +166,16 @@ protected function _changeGroup(\Magento\Framework\Model\AbstractModel $model) */ public function readAllStores() { - $select = $this->getConnection() - ->select() - ->from($this->getTable('store')); + $stores = []; + if ($this->getConnection()->isTableExists($this->getMainTable())) { + $select = $this->getConnection() + ->select() + ->from($this->getTable($this->getMainTable())); - return $this->getConnection()->fetchAll($select); + $stores = $this->getConnection()->fetchAll($select); + } + + return $stores; } /** diff --git a/app/code/Magento/Store/Model/ResourceModel/Website.php b/app/code/Magento/Store/Model/ResourceModel/Website.php index d6fefd60ae54a..431a9d62e7c39 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Website.php +++ b/app/code/Magento/Store/Model/ResourceModel/Website.php @@ -47,12 +47,15 @@ protected function _initUniqueFields() public function readAllWebsites() { $websites = []; - $select = $this->getConnection() - ->select() - ->from($this->getTable('store_website')); + $tableName = $this->getMainTable(); + if ($this->getConnection()->isTableExists($tableName)) { + $select = $this->getConnection() + ->select() + ->from($tableName); - foreach ($this->getConnection()->fetchAll($select) as $websiteData) { - $websites[$websiteData['code']] = $websiteData; + foreach ($this->getConnection()->fetchAll($select) as $websiteData) { + $websites[$websiteData['code']] = $websiteData; + } } return $websites; @@ -115,6 +118,7 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $model) /** * Retrieve default stores select object + * * Select fields website_id, store_id * * @param bool $includeDefault include/exclude default admin website diff --git a/app/code/Magento/Store/Model/ScopeTreeProvider.php b/app/code/Magento/Store/Model/ScopeTreeProvider.php index da772ec0410e0..d15030fe88ac6 100644 --- a/app/code/Magento/Store/Model/ScopeTreeProvider.php +++ b/app/code/Magento/Store/Model/ScopeTreeProvider.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Store\Model; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -78,25 +80,30 @@ public function get() 'scopes' => [], ]; - /** @var Group $group */ - foreach ($groups[$website->getId()] as $group) { - $groupScope = [ - 'scope' => ScopeInterface::SCOPE_GROUP, - 'scope_id' => $group->getId(), - 'scopes' => [], - ]; - - /** @var Store $store */ - foreach ($stores[$group->getId()] as $store) { - $storeScope = [ - 'scope' => ScopeInterface::SCOPE_STORES, - 'scope_id' => $store->getId(), + if (!empty($groups[$website->getId()])) { + /** @var Group $group */ + foreach ($groups[$website->getId()] as $group) { + $groupScope = [ + 'scope' => ScopeInterface::SCOPE_GROUP, + 'scope_id' => $group->getId(), 'scopes' => [], ]; - $groupScope['scopes'][] = $storeScope; + + if (!empty($stores[$group->getId()])) { + /** @var Store $store */ + foreach ($stores[$group->getId()] as $store) { + $storeScope = [ + 'scope' => ScopeInterface::SCOPE_STORES, + 'scope_id' => $store->getId(), + 'scopes' => [], + ]; + $groupScope['scopes'][] = $storeScope; + } + } + $websiteScope['scopes'][] = $groupScope; } - $websiteScope['scopes'][] = $groupScope; } + $defaultScope['scopes'][] = $websiteScope; } diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 0bc371da0aab9..5eda6f4a9b57d 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -53,7 +53,7 @@ class Store extends AbstractExtensibleModel implements const ENTITY = 'store'; /** - * Custom entry point param + * Parameter used to determine app context. */ const CUSTOM_ENTRY_POINT_PARAM = 'custom_entry_point'; @@ -104,7 +104,7 @@ class Store extends AbstractExtensibleModel implements const ADMIN_CODE = 'admin'; /** - * Cache tag + * Tag to use to cache stores. */ const CACHE_TAG = 'store'; diff --git a/app/code/Magento/Store/Model/StoreCookieManager.php b/app/code/Magento/Store/Model/StoreCookieManager.php index d5194379c69d8..d94357caf785c 100644 --- a/app/code/Magento/Store/Model/StoreCookieManager.php +++ b/app/code/Magento/Store/Model/StoreCookieManager.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,10 +10,13 @@ use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Api\StoreCookieManagerInterface; +/** + * DTO class to work with cookies. + */ class StoreCookieManager implements StoreCookieManagerInterface { /** - * Cookie name + * @var string */ const COOKIE_NAME = 'store'; @@ -41,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getStoreCodeFromCookie() { @@ -49,12 +51,12 @@ public function getStoreCodeFromCookie() } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreCookie(StoreInterface $store) { $cookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata() - ->setHttpOnly(true) + ->setHttpOnly(false) ->setDurationOneYear() ->setPath($store->getStorePath()); @@ -62,7 +64,7 @@ public function setStoreCookie(StoreInterface $store) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteStoreCookie(StoreInterface $store) { diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php new file mode 100644 index 0000000000000..456941bd41c25 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\HashGenerator\HashData; +use Magento\Store\Model\StoreSwitcherInterface; +use \Magento\Framework\App\DeploymentConfig as DeploymentConfig; +use Magento\Framework\Url\Helper\Data as UrlHelper; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Authorization\Model\UserContextInterface; +use \Magento\Framework\App\ActionInterface; + +/** + * Generate one time token and build redirect url + */ +class HashGenerator implements StoreSwitcherInterface +{ + /** + * @var \Magento\Framework\App\DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var UrlHelper + */ + private $urlHelper; + + /** + * @var UserContextInterface + */ + private $currentUser; + + /** + * @param DeploymentConfig $deploymentConfig + * @param UrlHelper $urlHelper + * @param UserContextInterface $currentUser + */ + public function __construct( + DeploymentConfig $deploymentConfig, + UrlHelper $urlHelper, + UserContextInterface $currentUser + ) { + $this->deploymentConfig = $deploymentConfig; + $this->urlHelper = $urlHelper; + $this->currentUser = $currentUser; + } + + /** + * Builds redirect url with token + * + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string redirect url + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string + { + $targetUrl = $redirectUrl; + $customerId = null; + $encodedUrl = $this->urlHelper->getEncodedUrl($redirectUrl); + + if ($this->currentUser->getUserType() == UserContextInterface::USER_TYPE_CUSTOMER) { + $customerId = $this->currentUser->getUserId(); + } + + if ($customerId) { + // phpcs:ignore + $urlParts = parse_url($targetUrl); + $host = $urlParts['host']; + $scheme = $urlParts['scheme']; + $key = (string)$this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $timeStamp = time(); + $fromStoreCode = $fromStore->getCode(); + $data = implode(',', [$customerId, $timeStamp, $fromStoreCode]); + $signature = hash_hmac('sha256', $data, $key); + $targetUrl = $scheme . "://" . $host . '/stores/store/switchrequest'; + $targetUrl = $this->urlHelper->addRequestParam( + $targetUrl, + ['customer_id' => $customerId] + ); + $targetUrl = $this->urlHelper->addRequestParam($targetUrl, ['time_stamp' => $timeStamp]); + $targetUrl = $this->urlHelper->addRequestParam($targetUrl, ['signature' => $signature]); + $targetUrl = $this->urlHelper->addRequestParam($targetUrl, ['___from_store' => $fromStoreCode]); + $targetUrl = $this->urlHelper->addRequestParam( + $targetUrl, + [ActionInterface::PARAM_NAME_URL_ENCODED => $encodedUrl] + ); + } + return $targetUrl; + } + + /** + * Validates one time token + * + * @param string $signature + * @param HashData $hashData + * @return bool + */ + public function validateHash(string $signature, HashData $hashData): bool + { + if (!empty($signature) && !empty($hashData)) { + $timeStamp = $hashData->getTimestamp(); + $fromStoreCode = $hashData->getFromStoreCode(); + $customerId = $hashData->getCustomerId(); + $value = implode(",", [$customerId, $timeStamp, $fromStoreCode]); + $key = (string)$this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + + if (time() - $timeStamp <= 5 && hash_equals($signature, hash_hmac('sha256', $value, $key))) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator/HashData.php b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator/HashData.php new file mode 100644 index 0000000000000..2d0068efbdd92 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator/HashData.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher\HashGenerator; + +use Magento\Framework\DataObject; + +/** + * HashData object for one time token + */ +class HashData extends DataObject +{ + /** + * Get CustomerId + * + * @return int + */ + public function getCustomerId(): int + { + return (int)$this->getData('customer_id'); + } + + /** + * Get Timestamp + * + * @return int + */ + public function getTimestamp(): int + { + return (int)$this->getData('time_stamp'); + } + + /** + * Get Fromstore + * + * @return string + */ + public function getFromStoreCode(): string + { + return (string)$this->getData('___from_store'); + } +} diff --git a/app/code/Magento/Store/Model/System/Store.php b/app/code/Magento/Store/Model/System/Store.php index cf75f26aed9b7..744019b107247 100644 --- a/app/code/Magento/Store/Model/System/Store.php +++ b/app/code/Magento/Store/Model/System/Store.php @@ -152,6 +152,12 @@ public function getStoreValuesForForm($empty = false, $all = false) } } } + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); return $options; } diff --git a/app/code/Magento/Store/Setup/Patch/Data/DisableSid.php b/app/code/Magento/Store/Setup/Patch/Data/DisableSid.php new file mode 100644 index 0000000000000..95df83043f15f --- /dev/null +++ b/app/code/Magento/Store/Setup/Patch/Data/DisableSid.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use \Magento\Framework\App\Config\MutableScopeConfigInterface; + +/** + * Disable default frontend SID + */ +class DisableSid implements DataPatchInterface, PatchVersionInterface +{ + + /** + * Config path for flag whether use SID on frontend + */ + const XML_PATH_USE_FRONTEND_SID = 'web/session/use_frontend_sid'; + + /** + * @var \Magento\Framework\App\Config\MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * scope type + */ + const SCOPE_STORE = 'store'; + + /** + * Disable Sid constructor. + * + * @param MutableScopeConfigInterface $mutableScopeConfig + */ + public function __construct( + MutableScopeConfigInterface $mutableScopeConfig + ) { + $this->mutableScopeConfig = $mutableScopeConfig; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->mutableScopeConfig->setValue(self::XML_PATH_USE_FRONTEND_SID, 0, self::SCOPE_STORE); + } + + /** + * @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/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml index 94856bb083da8..7ac300e3ab804 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -18,6 +18,7 @@ <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> <waitForPageLoad stepKey="waitStoreIndexPageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch"/> <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewInGrid"/> @@ -28,7 +29,8 @@ <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreDelete"/> <waitForPageLoad stepKey="waitForSuccessMessage"/> - <see userInput="You deleted the store view." stepKey="seeDeleteMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the store view." stepKey="seeDeleteMessage"/> </actionGroup> <actionGroup name="DeleteCustomStoreViewBackupEnabledYesActionGroup"> @@ -71,4 +73,42 @@ <waitForPageLoad stepKey="waitForStoreToLoad"/> <see selector="{{AdminStoresGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertStoreViewNotInGridMessage"/> </actionGroup> + + <actionGroup name="AdminSearchStoreViewByNameActionGroup"> + <annotations> + <description>Goes to the Admin Stores grid page. Clears filters and search by store view name.</description> + </annotations> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="resetSearchFilter"/> + <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{storeViewName}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + </actionGroup> + + <actionGroup name="AdminDeleteStoreViewIfExistsActionGroup" extends="AdminSearchStoreViewByNameActionGroup"> + <annotations> + <description>EXTENDS: AdminSearchStoreViewByNameActionGroup. Goes to the Admin Stores grid page. Deletes the provided Store (if exists) without creating a Backup. Validates that the Success Message is present and correct.</description> + </annotations> + + <executeInSelenium function="function($webdriver) use ($I) { + $items = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('.col-store_title>a')); + if(!empty($items)) { + $I->click('.col-store_title>a'); + $I->waitForPageLoad(10); + $I->click('#delete'); + $I->waitForPageLoad(30); + $I->selectOption('select#store_create_backup', 'No'); + $I->click('#delete'); + $I->waitForPageLoad(30); + $I->waitForElementVisible('aside.confirm .modal-title', 10); + $I->click('aside.confirm .modal-footer button.action-accept'); + $I->waitForPageLoad(60); + $I->waitForElementVisible('#messages div.message-success', 10); + $I->see('You deleted the store view.', '#messages div.message-success'); + } + }" after="clickSearchButton" stepKey="deleteStoreViewIfExists"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminWebsitePageActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminWebsitePageActionGroup.xml new file mode 100644 index 0000000000000..1a43ae1d2bbd1 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminWebsitePageActionGroup.xml @@ -0,0 +1,26 @@ +<?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="AdminGoCreatedWebsitePageActionGroup"> + <annotations> + <description>Filter website name in grid and go first found website page</description> + </annotations> + <arguments> + <argument name="websiteName" type="string" defaultValue="SecondWebsite"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml index f93b0a22f7558..a4d4374704291 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml @@ -26,8 +26,8 @@ <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteStoreGroupButtonOnEditStorePage"/> <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteStoreGroupButtonOnDeleteStorePage"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the store." stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the store." stepKey="seeSuccessMessage"/> </actionGroup> <actionGroup name="DeleteCustomStoreBackupEnabledYesActionGroup"> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml index 90dc74e3a3fee..77d148eedb99f 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="websiteName" defaultValue="customWebsite.name"/> </arguments> - + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> @@ -26,6 +26,6 @@ <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditStorePage"/> <selectOption userInput="No" selector="{{AdminStoresDeleteWebsiteSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteWebsiteSection.deleteButton}}" stepKey="clickDeleteButtonOnDeleteWebsitePage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the website." stepKey="checkSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the website." stepKey="checkSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index 1a1847bf38308..bdb1842cf2959 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -11,6 +11,9 @@ <data key="code">default</data> <data key="is_active">1</data> </entity> + <entity name="DefaultAllStoreView" type="store"> + <data key="name">All Store Views</data> + </entity> <entity name="customStore" type="store"> <!--data key="group_id">customStoreGroup.id</data--> <data key="name" unique="suffix">store</data> @@ -194,4 +197,13 @@ <data key="name">third_store_view</data> <data key="code">third_store_view</data> </entity> -</entities> \ No newline at end of file + <entity name="storeViewChinese" type="store"> + <data key="group_id">1</data> + <data key="name">Chinese</data> + <data key="code">chinese</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml deleted file mode 100644 index ed879a82d3f59..0000000000000 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml +++ /dev/null @@ -1,55 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<!-- Test XML Example --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminCreateStoreGroupTest"> - <annotations> - <features value="Store"/> - <stories value="Create a store group in admin"/> - <title value="Admin should be able to create a store group"/> - <description value="Admin should be able to create a store group"/> - <group value="store"/> - <severity value="AVERAGE"/> - </annotations> - <before> - <createData stepKey="b1" entity="customStoreGroup"/> - <createData stepKey="b2" entity="customStoreGroup"/> - </before> - <after> - <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStoreGroup"> - <argument name="storeGroupName" value="customStoreGroup.name" /> - </actionGroup> - <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStoreGroup2"> - <argument name="storeGroupName" value="customStoreGroup.name" /> - </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - </after> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - - <amOnPage stepKey="s9" url="{{AdminSystemStorePage.url}}"/> - <waitForPageLoad stepKey="waitForPageLoad" /> - - <click stepKey="s11" selector="{{AdminStoresGridSection.resetButton}}"/> - <waitForPageLoad stepKey="s15" time="10"/> - - <fillField stepKey="s17" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="$$b1.group[name]$$"/> - <click stepKey="s19" selector="{{AdminStoresGridSection.searchButton}}"/> - <waitForPageLoad stepKey="s21" time="10"/> - <see stepKey="s23" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" userInput="$$b1.group[name]$$"/> - - <click stepKey="s31" selector="{{AdminStoresGridSection.resetButton}}"/> - <waitForPageLoad stepKey="s35" time="10"/> - <fillField stepKey="s37" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="$$b2.group[name]$$"/> - <click stepKey="s39" selector="{{AdminStoresGridSection.searchButton}}"/> - <waitForPageLoad stepKey="s41" time="10"/> - <see stepKey="s43" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" userInput="$$b2.group[name]$$"/> - </test> -</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml index 1608d0b7b5a25..29d96c3cb94c2 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml @@ -19,7 +19,7 @@ </annotations> <before> - <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> diff --git a/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php b/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php index ba06d191c9f71..a8f76d0a28fee 100644 --- a/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php @@ -53,11 +53,11 @@ public function setUp() public function testGet() { - $this->deploymentConfig->expects($this->once()) + $this->deploymentConfig->expects($this->any()) ->method('get') ->with('db') ->willReturn(true); - $this->resourceConnection->expects($this->once())->method('getConnection')->willReturn($this->connection); + $this->resourceConnection->expects($this->any())->method('getConnection')->willReturn($this->connection); $selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $selectMock->expects($this->any())->method('from')->willReturnSelf(); diff --git a/app/code/Magento/Store/Test/Unit/Model/Config/PlaceholderTest.php b/app/code/Magento/Store/Test/Unit/Model/Config/PlaceholderTest.php index e44eacc79b82f..6e003e2e56275 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Config/PlaceholderTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Config/PlaceholderTest.php @@ -23,7 +23,7 @@ protected function setUp() { $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); $this->_requestMock->expects( - $this->once() + $this->any() )->method( 'getDistroBaseUrl' )->will( @@ -54,11 +54,27 @@ public function testProcess() ], 'path' => 'value', 'some_url' => '{{base_url}}some', + 'level1' => [ + 'level2' => [ + 'level3' => [ + // test that all levels are processed (i.e. implementation is not hardcoded to 3 levels) + 'level4' => '{{secure_base_url}}level4' + ] + ] + ] ]; $expectedResult = $data; $expectedResult['web']['unsecure']['base_link_url'] = 'http://localhost/website/de'; $expectedResult['web']['secure']['base_link_url'] = 'https://localhost/website/de'; + $expectedResult['level1']['level2']['level3']['level4'] = 'https://localhost/level4'; $expectedResult['some_url'] = 'http://localhost/some'; $this->assertEquals($expectedResult, $this->_model->process($data)); } + + public function testProcessEmptyArray() + { + $data = []; + $expectedResult = []; + $this->assertEquals($expectedResult, $this->_model->process($data)); + } } diff --git a/app/code/Magento/Store/Test/Unit/Model/ResourceModel/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/StoreTest.php new file mode 100644 index 0000000000000..926764b989686 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/StoreTest.php @@ -0,0 +1,190 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Store; +use Magento\Framework\DB\Adapter\AdapterInterface; + +class StoreTest extends \PHPUnit\Framework\TestCase +{ + /** @var Store */ + protected $model; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $resourceMock; + + /** @var Select | \PHPUnit_Framework_MockObject_MockObject */ + protected $select; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connectionMock; + + public function setUp() + { + $objectManagerHelper = new ObjectManager($this); + $this->select = $this->createMock(Select::class); + $this->resourceMock = $this->createPartialMock( + ResourceConnection::class, + [ + 'getConnection', + 'getTableName' + ] + ); + $this->connectionMock = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + [ + 'isTableExists', + 'select', + 'fetchAll', + 'fetchOne', + 'from', + 'getCheckSql', + 'where', + 'quoteIdentifier', + 'quote' + ] + ); + + $contextMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\Context::class); + $contextMock->expects($this->once())->method('getResources')->willReturn($this->resourceMock); + $configCacheTypeMock = $this->createMock('\Magento\Framework\App\Cache\Type\Config'); + $this->model = $objectManagerHelper->getObject( + Store::class, + [ + 'context' => $contextMock, + 'configCacheType' => $configCacheTypeMock + ] + ); + } + + public function testCountAll($countAdmin = false) + { + $mainTable = 'store'; + $tableIdentifier = 'code'; + $tableIdentifierValue = 'admin'; + $count = 1; + + $this->resourceMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable, 'COUNT(*)') + ->willReturnSelf(); + + $this->connectionMock->expects($this->any()) + ->method('quoteIdentifier') + ->with($tableIdentifier) + ->willReturn($tableIdentifier); + + $this->connectionMock->expects($this->once()) + ->method('quote') + ->with($tableIdentifierValue) + ->willReturn($tableIdentifierValue); + + $this->select->expects($this->any()) + ->method('where') + ->with(sprintf('%s <> %s', $tableIdentifier, $tableIdentifierValue)) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->select) + ->willReturn($count); + + $this->assertEquals($count, $this->model->countAll($countAdmin)); + } + + public function testReadAllStores() + { + $mainTable = 'store'; + $data = [ + ["store_id" => "0", "code" => "admin", "website_id" => 0, "name" => "Admin"], + ["store_id" => "1", "code" => "default", "website_id" => 1, "name" => "Default Store View"] + ]; + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(true); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllStores()); + } + + public function testReadAllStoresNoDbTable() + { + $mainTable = 'no_store_table'; + $data = []; + + $this->resourceMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(false); + + $this->connectionMock->expects($this->never()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->never()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->never()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllStores()); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/ResourceModel/WebsiteTest.php b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/WebsiteTest.php new file mode 100644 index 0000000000000..5fd5aa09a46be --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/WebsiteTest.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Website; + +class WebsiteTest extends \PHPUnit\Framework\TestCase +{ + /** @var Website */ + protected $model; + + /** + * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $resourceMock; + + /** @var Select | \PHPUnit_Framework_MockObject_MockObject */ + protected $select; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connectionMock; + + public function setUp() + { + $objectManagerHelper = new ObjectManager($this); + $this->select = $this->createMock(\Magento\Framework\DB\Select::class); + $this->resourceMock = $this->createPartialMock( + ResourceConnection::class, + [ + 'getConnection', + 'getTableName' + ] + ); + $this->connectionMock = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + [ + 'isTableExists', + 'select', + 'fetchAll', + 'fetchOne', + 'from', + 'getCheckSql', + 'joinLeft', + 'where' + ] + ); + $contextMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\Context::class); + $contextMock->expects($this->once())->method('getResources')->willReturn($this->resourceMock); + $this->model = $objectManagerHelper->getObject( + Website::class, + [ + 'context' => $contextMock + ] + ); + } + + public function testReadAllWebsites() + { + $data = [ + "admin" => ["website_id" => "0", "code" => "admin", "name" => "Admin"], + "base" => ["website_id" => "1", "code" => "base", "name" => "Main Website"] + ]; + $mainTable = 'store_website'; + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(true); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllWebsites()); + } + + public function testReadAllWebsitesNoDbTable() + { + $data = []; + $mainTable = 'no_store_website_table'; + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(false); + + $this->connectionMock->expects($this->never()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->never()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->never()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllWebsites()); + } + + public function testGetDefaultStoresSelect($includeDefault = false) + { + $storeId = 1; + $storeWebsiteTable = 'store_website'; + $storeGroupTable = 'store_group'; + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('getCheckSql') + ->with( + 'store_group_table.default_store_id IS NULL', + '0', + 'store_group_table.default_store_id' + ) + ->willReturn($storeId); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getTableName') + ->withConsecutive([$storeWebsiteTable], [$storeGroupTable]) + ->willReturnOnConsecutiveCalls($storeWebsiteTable, $storeGroupTable); + + $this->select->expects($this->once()) + ->method('from') + ->with( + ['website_table' => $storeWebsiteTable], + ['website_id'] + ) + ->willReturnSelf(); + + $this->select->expects($this->once()) + ->method('joinLeft') + ->with( + ['store_group_table' => $storeGroupTable], + 'website_table.website_id=store_group_table.website_id' . + ' AND website_table.default_group_id = store_group_table.group_id', + ['store_id' => $storeId] + ) + ->willReturnSelf(); + + $this->assertInstanceOf('\Magento\Framework\DB\Select', $this->model->getDefaultStoresSelect($includeDefault)); + } + + public function testCountAll($includeDefault = false) + { + $count = 2; + $mainTable = 'store_website'; + + $this->resourceMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable, 'COUNT(*)') + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->select) + ->willReturn($count); + + $this->assertEquals($count, $this->model->countAll($includeDefault)); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php index 9cc4bb6ac8e5b..6befb28e35383 100644 --- a/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php @@ -6,6 +6,9 @@ namespace Magento\Store\Test\Unit\Model\System; +/** + * Class StoreTest covers Magento\Store\Model\System\Store. + */ class StoreTest extends \PHPUnit\Framework\TestCase { /** @@ -262,14 +265,15 @@ public function getStoreValuesForFormDataProvider() 'storeGroupId' => $groupId, 'groupWebsiteId' => $websiteId, 'expectedResult' => [ - ['label' => '', 'value' => ''], - ['label' => __('All Store Views'), 'value' => 0], - ['label' => $websiteName, 'value' => []], + ['label' => '', 'value' => '','__disableTmpl' => true], + ['label' => __('All Store Views'), 'value' => 0,'__disableTmpl' => true], + ['label' => $websiteName, 'value' => [],'__disableTmpl' => true], [ 'label' => str_repeat($nonEscapableNbspChar, 4) . $groupName, 'value' => [ ['label' => str_repeat($nonEscapableNbspChar, 4) . $storeName, 'value' => $storeId] - ] + ], + '__disableTmpl' => true ], ] ], diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php b/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php index 1fc13390e30b5..907eb74e20fa2 100644 --- a/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php @@ -68,6 +68,26 @@ public function toOptionArray() return $this->options; } + /** + * Sanitize website/store option name + * + * @param string $name + * + * @return string + */ + protected function sanitizeName($name) + { + $matches = []; + preg_match('/\$[:]*{(.)*}/', $name, $matches); + if (count($matches) > 0) { + $name = $this->escaper->escapeHtml($this->escaper->escapeJs($name)); + } else { + $name = $this->escaper->escapeHtml($name); + } + + return $name; + } + /** * Generate current options * @@ -88,20 +108,20 @@ protected function generateCurrentOptions() /** @var \Magento\Store\Model\Store $store */ foreach ($storeCollection as $store) { if ($store->getGroupId() == $group->getId()) { - $name = $this->escaper->escapeHtml($store->getName()); + $name = $this->sanitizeName($store->getName()); $stores[$name]['label'] = str_repeat(' ', 8) . $name; $stores[$name]['value'] = $store->getId(); } } if (!empty($stores)) { - $name = $this->escaper->escapeHtml($group->getName()); + $name = $this->sanitizeName($group->getName()); $groups[$name]['label'] = str_repeat(' ', 4) . $name; $groups[$name]['value'] = array_values($stores); } } } if (!empty($groups)) { - $name = $this->escaper->escapeHtml($website->getName()); + $name = $this->sanitizeName($website->getName()); $this->currentOptions[$name]['label'] = $name; $this->currentOptions[$name]['value'] = array_values($groups); } diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index bb7c33be70562..1ca87ec068d47 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -12,6 +12,8 @@ "magento/module-directory": "*", "magento/module-media-storage": "*", "magento/module-ui": "*", + "magento/module-customer": "*", + "magento/module-authorization": "*", "magento/module-backend": "*" }, "suggest": { diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index b9e7ac1c6aca0..07e4c8b0b6529 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -83,7 +83,7 @@ <use_http_via>0</use_http_via> <use_http_x_forwarded_for>0</use_http_x_forwarded_for> <use_http_user_agent>0</use_http_user_agent> - <use_frontend_sid>1</use_frontend_sid> + <use_frontend_sid>0</use_frontend_sid> </session> <browser_capabilities> <cookies>1</cookies> @@ -135,14 +135,14 @@ </protected_extensions> <public_files_valid_paths> <protected> - <app>/app/*/*</app> - <bin>/bin/*/*</bin> - <dev>/dev/*/*</dev> - <generated>/generated/*/*</generated> - <lib>/lib/*/*</lib> - <setup>/setup/*/*</setup> - <update>/update/*/*</update> - <vendor>/vendor/*/*</vendor> + <app>*/app/*/*</app> + <bin>*/bin/*/*</bin> + <dev>*/dev/*/*</dev> + <generated>*/generated/*/*</generated> + <lib>*/lib/*/*</lib> + <setup>*/setup/*/*</setup> + <update>*/update/*/*</update> + <vendor>*/vendor/*/*</vendor> </protected> </public_files_valid_paths> </file> diff --git a/app/code/Magento/Store/etc/db_schema.xml b/app/code/Magento/Store/etc/db_schema.xml index 6eea94b8deec7..5b2e178ff24b8 100644 --- a/app/code/Magento/Store/etc/db_schema.xml +++ b/app/code/Magento/Store/etc/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="store_website" resource="default" engine="innodb" comment="Websites"> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Code"/> <column xsi:type="varchar" name="name" nullable="true" length="64" comment="Website Name"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="default_group_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Default Group Id"/> + identity="false" default="0" comment="Default Group ID"/> <column xsi:type="smallint" name="is_default" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Defines Is Website Default"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -33,14 +33,14 @@ </table> <table name="store_group" resource="default" engine="innodb" comment="Store Groups"> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Group Id"/> + comment="Group ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="varchar" name="name" nullable="false" length="255" comment="Store Group Name"/> <column xsi:type="int" name="root_category_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Root Category Id"/> + default="0" comment="Root Category ID"/> <column xsi:type="smallint" name="default_store_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Default Store Id"/> + identity="false" default="0" comment="Default Store ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Store group unique code"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="group_id"/> @@ -59,12 +59,12 @@ </table> <table name="store" resource="default" engine="innodb" comment="Stores"> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Code"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Group Id"/> + default="0" comment="Group ID"/> <column xsi:type="varchar" name="name" nullable="false" length="255" comment="Store Name"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Sort Order"/> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index defe0694d018d..3fa9c8314fdd1 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -436,6 +436,7 @@ <item name="cleanTargetUrl" xsi:type="object">Magento\Store\Model\StoreSwitcher\CleanTargetUrl</item> <item name="manageStoreCookie" xsi:type="object">Magento\Store\Model\StoreSwitcher\ManageStoreCookie</item> <item name="managePrivateContent" xsi:type="object">Magento\Store\Model\StoreSwitcher\ManagePrivateContent</item> + <item name="hashGenerator" xsi:type="object" sortOrder="1000">Magento\Store\Model\StoreSwitcher\HashGenerator</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Store/etc/frontend/sections.xml b/app/code/Magento/Store/etc/frontend/sections.xml index b1a9fc3cb1d71..b7dbfe405263b 100644 --- a/app/code/Magento/Store/etc/frontend/sections.xml +++ b/app/code/Magento/Store/etc/frontend/sections.xml @@ -8,4 +8,5 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> <action name="stores/store/switch"/> + <action name="stores/store/switchrequest"/> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index 3a0143821d8b9..f3771b704c3e9 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -23,4 +23,11 @@ </argument> </arguments> </type> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="store_name" xsi:type="string">store/information/name</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index 376635e5c8f75..aaef3aa13dbaf 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -30,4 +30,5 @@ type StoreConfig @doc(description: "The type contains information about a store secure_base_link_url : String @doc(description: "Secure base link URL for the store") secure_base_static_url : String @doc(description: "Secure base static URL for the store") secure_base_media_url : String @doc(description: "Secure base media URL for the store") + store_name : String @doc(description: "Name of the store") } diff --git a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml index 5a592b9b7c987..059b9ad445806 100644 --- a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml +++ b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml @@ -30,6 +30,7 @@ <!--Remove Magento page content--> <referenceContainer name="page.wrapper" remove="true"/> <referenceBlock name="translate" remove="true"/> + <referenceBlock name="theme.active.editor" remove="true" /> <referenceBlock name="requirejs-config" remove="true"/> <referenceContainer name="root"> <block name="swaggerUiContent" class="Magento\Swagger\Block\Index" template="Magento_Swagger::swagger-ui/index.phtml" /> diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php index 0848f566f67bb..a2cae7f7b5a20 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types = 1); namespace Magento\Swatches\Block\Product\Renderer; use Magento\Catalog\Block\Product\Context; @@ -57,6 +58,11 @@ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\ */ const SWATCH_THUMBNAIL_NAME = 'swatchThumb'; + /** + * Config path which contains number of swatches per product + */ + private const XML_PATH_SWATCHES_PER_PRODUCT = 'catalog/frontend/swatches_per_product'; + /** * @var Product */ @@ -200,7 +206,7 @@ public function getJsonSwatchConfig() public function getNumberSwatchesPerProduct() { return $this->_scopeConfig->getValue( - 'catalog/frontend/swatches_per_product', + self::XML_PATH_SWATCHES_PER_PRODUCT, ScopeInterface::SCOPE_STORE ); } diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php index 2e4980c2fbfd0..6e0a1e8d01360 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php @@ -9,7 +9,6 @@ use Magento\Catalog\Helper\Product as CatalogProduct; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Layer\Resolver; -use Magento\Catalog\Model\Layer\Category as CategoryLayer; use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; @@ -154,7 +153,7 @@ public function getJsonConfig() $this->unsetData('allow_products'); return parent::getJsonConfig(); } - + /** * Composes configuration for js price format * @@ -242,9 +241,12 @@ private function getLayeredAttributesIfExists(Product $configurableProduct, arra $layeredAttributes = []; - $configurableAttributes = array_map(function ($attribute) { - return $attribute->getAttributeCode(); - }, $configurableAttributes); + $configurableAttributes = array_map( + function ($attribute) { + return $attribute->getAttributeCode(); + }, + $configurableAttributes + ); $commonAttributeCodes = array_intersect( $configurableAttributes, @@ -257,16 +259,4 @@ private function getLayeredAttributesIfExists(Product $configurableProduct, arra return $layeredAttributes; } - - /** - * @inheritdoc - */ - public function getCacheKeyInfo() - { - $cacheKeyInfo = parent::getCacheKeyInfo(); - /** @var CategoryLayer $catalogLayer */ - $catalogLayer = $this->layerResolver->get(); - $cacheKeyInfo[] = $catalogLayer->getStateKey(); - return $cacheKeyInfo; - } } diff --git a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php index ebbb1775aa7f8..0acd7ef315700 100644 --- a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php +++ b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php @@ -14,6 +14,8 @@ /** * Plugin model for Catalog Resource Attribute + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class EavAttribute { @@ -29,6 +31,11 @@ class EavAttribute */ const BASE_OPTION_TITLE = 'option'; + /** + * Prefix added to option value added through API + */ + private const API_OPTION_PREFIX = 'id_'; + /** * @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory */ @@ -189,7 +196,9 @@ protected function processSwatchOptions(Attribute $attribute) if (!empty($optionsArray) && is_array($optionsArray)) { $optionsArray = $this->prepareOptionIds($optionsArray); - $attributeSavedOptions = $attribute->getSource()->getAllOptions(); + $adminStoreAttribute = clone $attribute; + $adminStoreAttribute->setStoreId(self::DEFAULT_STORE_ID); + $attributeSavedOptions = $adminStoreAttribute->getSource()->getAllOptions(); $this->prepareOptionLinks($optionsArray, $attributeSavedOptions); } @@ -227,10 +236,9 @@ protected function prepareOptionLinks(array $optionsArray, array $attributeSaved { $dependencyArray = []; if (is_array($optionsArray['value'])) { - $optionCounter = 1; - foreach (array_keys($optionsArray['value']) as $baseOptionId) { - $dependencyArray[$baseOptionId] = $attributeSavedOptions[$optionCounter]['value']; - $optionCounter++; + $options = array_column($attributeSavedOptions, 'value', 'label'); + foreach ($optionsArray['value'] as $id => $labels) { + $dependencyArray[$id] = $options[$labels[self::DEFAULT_STORE_ID]]; } } @@ -285,7 +293,7 @@ protected function processVisualSwatch(Attribute $attribute) * Clean swatch option values after switching to the dropdown type. * * @param array $attributeOptions - * @param int|null $swatchType + * @param int|null $swatchType * @throws \Magento\Framework\Exception\LocalizedException */ private function cleanEavAttributeOptionSwatchValues(array $attributeOptions, int $swatchType = null) @@ -309,6 +317,8 @@ private function cleanTextSwatchValuesAfterSwitch(array $attributeOptions) } /** + * Get the visual swatch type based on its value + * * @param string $value * @return int */ @@ -368,7 +378,7 @@ protected function processTextualSwatch(Attribute $attribute) */ protected function getAttributeOptionId($optionId) { - if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE) { + if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE || substr($optionId, 0, 3) == self::API_OPTION_PREFIX) { $optionId = isset($this->dependencyArray[$optionId]) ? $this->dependencyArray[$optionId] : null; } return $optionId; @@ -447,13 +457,10 @@ protected function saveDefaultSwatchOptionValue(Attribute $attribute) if (!empty($defaultValue)) { /** @var \Magento\Swatches\Model\Swatch $swatch */ $swatch = $this->swatchFactory->create(); - // created and removed on frontend option not exists in dependency array - if (substr($defaultValue, 0, 6) == self::BASE_OPTION_TITLE && - isset($this->dependencyArray[$defaultValue]) - ) { - $defaultValue = $this->dependencyArray[$defaultValue]; - } - $swatch->getResource()->saveDefaultSwatchOption($attribute->getId(), $defaultValue); + $swatch->getResource()->saveDefaultSwatchOption( + $attribute->getId(), + $this->getAttributeOptionId($defaultValue) + ); } } @@ -503,6 +510,10 @@ protected function isOptionsValid(array $options, Attribute $attribute) } /** + * Modifies Attribute::usesSource() response + * + * Returns true if attribute type is swatch + * * @param Attribute $attribute * @param bool $result * @return bool diff --git a/app/code/Magento/Swatches/Observer/AddFieldsToAttributeObserver.php b/app/code/Magento/Swatches/Observer/AddFieldsToAttributeObserver.php index 3ef202af95a1a..303f1eda2e40a 100644 --- a/app/code/Magento/Swatches/Observer/AddFieldsToAttributeObserver.php +++ b/app/code/Magento/Swatches/Observer/AddFieldsToAttributeObserver.php @@ -6,7 +6,7 @@ namespace Magento\Swatches\Observer; use Magento\Config\Model\Config\Source; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; @@ -21,15 +21,15 @@ class AddFieldsToAttributeObserver implements ObserverInterface protected $yesNo; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param Source\Yesno $yesNo */ - public function __construct(ModuleManagerInterface $moduleManager, Source\Yesno $yesNo) + public function __construct(Manager $moduleManager, Source\Yesno $yesNo) { $this->moduleManager = $moduleManager; $this->yesNo = $yesNo; diff --git a/app/code/Magento/Swatches/Observer/AddSwatchAttributeTypeObserver.php b/app/code/Magento/Swatches/Observer/AddSwatchAttributeTypeObserver.php index ca75da3321698..fb8c185cc545b 100644 --- a/app/code/Magento/Swatches/Observer/AddSwatchAttributeTypeObserver.php +++ b/app/code/Magento/Swatches/Observer/AddSwatchAttributeTypeObserver.php @@ -6,7 +6,7 @@ namespace Magento\Swatches\Observer; use Magento\Config\Model\Config\Source; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; @@ -16,14 +16,14 @@ class AddSwatchAttributeTypeObserver implements ObserverInterface { /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; /** - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager */ - public function __construct(ModuleManagerInterface $moduleManager) + public function __construct(Manager $moduleManager) { $this->moduleManager = $moduleManager; } diff --git a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php new file mode 100644 index 0000000000000..795c48f12ebcc --- /dev/null +++ b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Swatches\Plugin\Eav\Model\Entity\Attribute; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Store\Model\Store; +use Magento\Swatches\Helper\Data; + +/** + * OptionManagement Plugin + */ +class OptionManagement +{ + /** + * @var AttributeRepository + */ + private $attributeRepository; + /** + * @var Data + */ + private $swatchHelper; + + /** + * @param AttributeRepository $attributeRepository + * @param Data $swatchHelper + */ + public function __construct( + AttributeRepository $attributeRepository, + Data $swatchHelper + ) { + $this->attributeRepository = $attributeRepository; + $this->swatchHelper = $swatchHelper; + } + + /** + * Add swatch value to the attribute option + * + * @param \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject + * @param string $attributeCode + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeAdd( + \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject, + ?string $attributeCode, + \Magento\Eav\Api\Data\AttributeOptionInterface $option + ) { + if (empty($attributeCode)) { + return; + } + $attribute = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode + ); + if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) { + return; + } + $optionId = $this->getOptionId($option); + $optionsValue = $option->getValue(); + if ($this->swatchHelper->isVisualSwatch($attribute)) { + $attribute->setData('swatchvisual', ['value' => [$optionId => $optionsValue]]); + } else { + $options = []; + $options['value'][$optionId][Store::DEFAULT_STORE_ID] = $optionsValue; + if (is_array($option->getStoreLabels())) { + foreach ($option->getStoreLabels() as $label) { + if (!isset($options['value'][$optionId][$label->getStoreId()])) { + $options['value'][$optionId][$label->getStoreId()] = null; + } + } + } + $attribute->setData('swatchtext', $options); + } + } + + /** + * Returns option id + * + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return string + */ + private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string + { + return 'id_' . ($option->getValue() ?: 'new_option'); + } +} diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml index 8b95b86065b7d..4a67c0dfbe8e4 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml @@ -85,6 +85,15 @@ <selectOption selector="{{AdminNewAttributePanel.useInProductListing}}" stepKey="switchOnUsedInProductListing" userInput="Yes" after="switchOnVisibleOnCatalogPagesOnStorefront"/> </actionGroup> + <actionGroup name="AddVisualSwatchWithProductWithStorefrontPreviewImageConfigActionGroup" extends="AddVisualSwatchToProductActionGroup"> + <selectOption selector="{{AdminNewAttributePanel.updateProductPreviewImage}}" userInput="Yes" stepKey="selectUpdatePreviewImage" after="selectInputType"/> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillDefaultStoreLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" after="goToStorefrontPropertiesTab" stepKey="waitTabLoad"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" userInput="Filterable (with results)" stepKey="selectUseInLayer" after="waitTabLoad"/> + <selectOption selector="{{AdminNewAttributePanel.useInProductListing}}" userInput="Yes" stepKey="switchOnUsedInProductListing" after="selectUseInLayer"/> + <selectOption selector="{{AdminNewAttributePanel.usedForStoringInProductListing}}" userInput="Yes" stepKey="switchOnUsedForStoringInProductListing" after="switchOnUsedInProductListing"/> + </actionGroup> + <actionGroup name="AddTextSwatchToProductActionGroup"> <annotations> <description>Add text swatch property attribute.</description> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml index 0c2dea5f41235..5149fa3a1e518 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewAttributePanel"> + <element name="updateProductPreviewImage" type="select" selector="#update_product_preview_image"/> <element name="addVisualSwatchOption" type="button" selector="button#add_new_swatch_visual_option_button"/> <element name="addTextSwatchOption" type="button" selector="button#add_new_swatch_text_option_button"/> <element name="visualSwatchOptionAdminValue" type="input" selector="[data-role='swatch-visual-options-container'] input[name='optionvisual[value][option_{{row}}][0]']" parameterized="true"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 6fdf2276f39d9..5b714f01fd46f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -16,5 +16,6 @@ <element name="nthSwatchOptionText" type="button" selector="div.swatch-option.text:nth-of-type({{n}})" parameterized="true"/> <element name="productSwatch" type="button" selector="//div[@class='swatch-option'][@aria-label='{{var1}}']" parameterized="true"/> <element name="visualSwatchOption" type="button" selector=".swatch-option[option-tooltip-value='#{{visualSwatchOption}}']" parameterized="true"/> + <element name="swatchOptionTooltip" type="block" selector="div.swatch-option-tooltip"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml index 3ef347b7aca12..87d3f0bb5bcb9 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a new product attribute of type "Text Swatch" --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml index 90e94466351b6..65f0e2b09b82a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml @@ -35,7 +35,7 @@ <waitForPageLoad stepKey="waitToClickSave"/> <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit"/> <!-- Logout --> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Go to the edit page for the "color" attribute --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml new file mode 100644 index 0000000000000..2ffc61614bd1d --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -0,0 +1,161 @@ +<?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="AdminDisablingSwatchTooltipsTest"> + <annotations> + <features value="Swatches"/> + <stories value="Swatch Tooltip Status Change"/> + <title value="Admin disabling swatch tooltips test."/> + <description value="Verify possibility to disable/enable swatch tooltips."/> + <severity value="AVERAGE"/> + <group value="Swatches"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Log in --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Clean up our modifications to the existing color attribute --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <waitForPageLoad stepKey="waitForProductAttributes"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" + stepKey="fillFilter"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> + <click selector="{{AdminManageSwatchSection.nthDelete('1')}}" stepKey="deleteSwatch1"/> + <waitForPageLoad stepKey="waitToClickSave"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logOut"/> + + <!-- Delete category --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + + <!-- Enable swatch tooltips --> + <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 1" stepKey="disableTooltips"/> + <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnabling"/> + </after> + + <!-- Go to the edit page for the "color" attribute --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <waitForPageLoad stepKey="waitForProductAttributes"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" + stepKey="fillFilter"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> + + <!-- Change to visual swatches --> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="swatch_visual" + stepKey="selectVisualSwatch"/> + + <!-- Set swatch using the color picker --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch1"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSwatch1"> + <argument name="index" value="0"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthChooseColor('1')}}" stepKey="clickChooseColor1"/> + <actionGroup ref="setColorPickerByHex" stepKey="fillHex1"> + <argument name="nthColorPicker" value="1"/> + <argument name="hexColor" value="e74c3c"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="red" stepKey="fillAdmin1"/> + <waitForPageLoad stepKey="waitToClickSave"/> + + <!-- Save --> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit1"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> + + <!-- Assert that the Save was successful after round trip to server --> + <actionGroup ref="assertSwatchColor" stepKey="assertSwatchAdmin"> + <argument name="nthSwatch" value="1"/> + <argument name="expectedStyle" value="background: rgb(231, 77, 60);"/> + </actionGroup> + + <!-- Create a configurable product to verify the storefront with --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" + stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" + stepKey="fillName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" + stepKey="fillSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" + stepKey="fillPrice"/> + <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" + stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{_defaultProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" + stepKey="fillUrlKey"/> + + <!-- Create configurations based on the Swatch we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" + stepKey="clickCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="color" + stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesByAttributeToEachSku}}" + stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" userInput="Color" + stepKey="selectAttributes"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attribute1}}" userInput="10" + stepKey="fillAttributePrice1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" + stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="99" + stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" + dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" + stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + <seeInTitle userInput="{{_defaultProduct.name}}" stepKey="seeProductNameInTitle"/> + + <!-- Go to the product page and see swatch options --> + <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + + <!-- Verify that the storefront shows the swatches too --> + <actionGroup ref="assertStorefrontSwatchColor" stepKey="assertSwatchStorefront"> + <argument name="nthSwatch" value="1"/> + <argument name="expectedRgb" value="rgb(231, 77, 60)"/> + </actionGroup> + + <!-- Verify swatch tooltips are visible--> + <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverEnabledSwatch"/> + <wait time="1" stepKey="waitForTooltip1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipVisible"/> + + <!-- Disable swatch tooltips --> + <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 0" stepKey="disableTooltips"/> + <magentoCLI command="cache:flush" stepKey="flushCacheAfterDisabling"/> + + <!-- Verify swatch tooltips are not visible --> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReload"/> + <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverDisabledSwatch"/> + <wait time="1" stepKey="waitForTooltip2"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipNotVisible"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml new file mode 100644 index 0000000000000..f94314fe94806 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml @@ -0,0 +1,110 @@ +<?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="AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product attributes"/> + <title value="Saving configurable product with custom product attribute (images as swatches)"/> + <description value="Saving configurable product with custom product attribute (images as swatches)"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13641"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create a new product attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="openProductAttributePage"/> + <click selector="{{AdminProductAttributeGridSection.createNewAttributeBtn}}" stepKey="createNewAttribute"/> + <!-- Set Catalog Input Type for Store Owner: Visual Swatch --> + <actionGroup ref="AdminFillProductAttributePropertiesActionGroup" stepKey="fillAttributeProperties"> + <argument name="attributeName" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + <argument name="attributeType" value="{{VisualSwatchProductAttribute.frontend_input}}"/> + </actionGroup> + <!-- Add a few Swatches and add images to Manage Swatch (Values of Your Attribute) + 1. Set swatch #1 using the color picker --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddFirstSwatch"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickFirstSwatch"> + <argument name="index" value="0"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthChooseColor('1')}}" stepKey="clickChooseColor"/> + <actionGroup ref="setColorPickerByHex" stepKey="fillFirstHex"> + <argument name="nthColorPicker" value="1"/> + <argument name="hexColor" value="e74c3c"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="red" stepKey="fillFirstAdminField"/> + <!-- Set swatch #2 using upload file --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSecondSwatch"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSwatch2"> + <argument name="index" value="1"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile2"/> + <attachFile selector="input[name='datafile']" userInput="{{placeholderSmallImage.file}}" stepKey="attachFile"/> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" userInput="adobe-small" stepKey="fillAdminLabel"/> + <!-- Set Scope: Global in Advanced Attribute Properties --> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <!-- Click "Save Attribute" button --> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </before> + <after> + <!-- Delete product attribute and clear grid filter --> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearAttributesGridFilter"/> + <!--Clear products grid filter--> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearProductsGridFilter"/> + <!-- Admin logout --> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Add created product attribute to the Default set --> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + + <!-- Create configurable product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <!-- Fill all the necessary information such as weight, name, SKU etc --> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <!-- Click "Create Configurations" button, select created product attribute using the same Quantity for all products. Click "Generate products" button --> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{VisualSwatchProductAttribute.attribute_code}}"/> + </actionGroup> + <!-- Using this action to concatenate 2 strings to have unique identifier for grid --> + <executeJS function="return '{{VisualSwatchProductAttribute.attribute_code}}: red'" stepKey="attributeCodeRed"/> + <executeJS function="return '{{VisualSwatchProductAttribute.attribute_code}}: {{placeholderSmallImage.name}}'" stepKey="attributeCodeAdobeSmall"/> + <!-- Add images for the products --> + <attachFile selector="{{AdminDataGridTableSection.rowTemplate({$attributeCodeRed})}}{{AdminProductFormConfigurationsSection.fileUploaderInput}}" userInput="{{MagentoLogo.file}}" stepKey="uploadImageForFirstProduct"/> + <attachFile selector="{{AdminDataGridTableSection.rowTemplate({$attributeCodeAdobeSmall})}}{{AdminProductFormConfigurationsSection.fileUploaderInput}}" userInput="{{TestImageAdobe.file}}" stepKey="uploadImageForSecondProduct"/> + + <!-- Click "Save" button --> + <actionGroup ref="saveProductForm" stepKey="clickSaveButton"/> + + <!-- Delete all created product --> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProducts"> + <argument name="sku" value="{{ApiConfigurableProduct.sku}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml new file mode 100644 index 0000000000000..569952019b29b --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.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="AdminSetUpWatermarkForSwatchImageTest"> + <annotations> + <features value="Swatches"/> + <title value="Possibility to set up watermark for a swatch image type"/> + <description value="Possibility to set up watermark for a swatch image type"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17607"/> + <useCaseId value="MC-15523"/> + <group value="swatches"/> + </annotations> + <before> + <!-- Login as Admin --> + <comment userInput="Login as Admin" stepKey="commentLoginAsAdmin"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Log out --> + <comment userInput="Log out" stepKey="commentLogOut"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Go to Admin > Content > Configuration page --> + <comment userInput="Go to Configuration Page" stepKey="commentOpenConfigurationPage"/> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage"/> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterDefaultStoreView"> + <argument name="customStore" value="'Default'" /> + </actionGroup> + <!-- Select Edit next to the Default Store View --> + <comment userInput="Select Edit next to the Default Store View" stepKey="commentEditDefaultView"/> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickToEditDefaultStoreView"/> + <waitForPageLoad stepKey="waitForDefaultStorePage"/> + <!-- Expand the Product Image Watermarks section--> + <comment userInput="Expand the Product Image Watermarks section" stepKey="commentOpenWatermarksSection"/> + <click selector="{{AdminDesignConfigSection.watermarkSectionHeader}}" stepKey="clickToProductImageWatermarks"/> + <waitForPageLoad stepKey="waitForWatermarksPage"/> + <!-- See Base, Thumbnail, Small image types are displayed --> + <comment userInput="See Base, Thumbnail, Small image types are displayed" stepKey="commentSeeImageTypes"/> + <seeElement selector="{{AdminDesignConfigSection.imageWatermarkType('Base')}}" stepKey="seeElementBaseWatermark"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.imageWatermarkType('Thumbnail')}}" stepKey="waitForThumbnailVisible" /> + <seeElement selector="{{AdminDesignConfigSection.imageWatermarkType('Thumbnail')}}" stepKey="seeElementThumbnailWatermark"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.imageWatermarkType('Small')}}" stepKey="waitForSmallVisible" /> + <seeElement selector="{{AdminDesignConfigSection.imageWatermarkType('Small')}}" stepKey="seeElementSmallWatermark"/> + <!-- See Swatch Image type is absent --> + <comment userInput="See Swatch Image type is absent" stepKey="commentSeeTypeAbsent"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <dontSeeElement selector="{{AdminDesignConfigSection.imageWatermarkType('Swatch')}}" stepKey="dontSeeImageWatermarkSwatchImage"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminWatermarkUploadTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminWatermarkUploadTest.xml deleted file mode 100644 index e9df186bae5e6..0000000000000 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminWatermarkUploadTest.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?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="AdminWatermarkUploadTest"> - <waitForElement selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Swatch Image')}}" stepKey="waitForInputVisible4" after="waitForPreviewImage3"/> - <attachFile selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Swatch Image')}}" userInput="adobe-small.jpg" stepKey="attachFile4" after="waitForInputVisible4"/> - <waitForElementVisible selector="{{AdminDesignConfigSection.imageUploadPreviewByFieldsetName('Swatch Image')}}" stepKey="waitForPreviewImage4" after="attachFile4"/> - </test> -</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml index 470421776cf8f..1bcdd6fcf9a3a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml @@ -29,6 +29,9 @@ <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('3')}}" userInput="123456789012345678901" stepKey="fillSwatch3" after="clickAddSwatch3"/> <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('3')}}" userInput="123456789012345678901BrownD" stepKey="fillDescription3" after="fillSwatch3"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '3')}}" userInput="123456789012345678901" stepKey="seeGreen" after="seeBlue"/> <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '4')}}" userInput="123456789012345678901" stepKey="seeBrown" after="seeGreen"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index b1ae06428c0ab..c9602ddcd127c 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -30,7 +30,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Begin creating a new product attribute --> @@ -103,6 +103,9 @@ <argument name="image" value="TestImageAdobe"/> </actionGroup> + <!-- Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml index 28df5ffd53436..7bf63d25417e3 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml @@ -28,7 +28,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Begin creating a new product attribute --> @@ -82,6 +82,9 @@ <argument name="attributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> </actionGroup> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml index d12cb0433fed1..fd38c48919416 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml @@ -30,7 +30,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Begin creating a new product attribute --> @@ -94,6 +94,9 @@ <argument name="attributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> </actionGroup> + <!-- Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml new file mode 100644 index 0000000000000..2928011662864 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -0,0 +1,95 @@ +<?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="StorefrontImageColorWhenFilterByColorFilterTest"> + <annotations> + <features value="Swatches"/> + <stories value="Color image when filtering by color filter"/> + <title value="Image color when filtering by color filter on the Storefront"/> + <description value="Image color when filtering by color filter on the Storefront"/> + <severity value="MAJOR"/> + <useCaseId value="MC-18821"/> + <testCaseId value="MC-11531"/> + <group value="Swatches"/> + </annotations> + <before> + <!--Create category and configurable product with two options--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="visualSwatchAttribute"/> + </actionGroup> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="adminDataGridSelectPerPage" stepKey="selectNumberOfProductsPerPage"> + <argument name="perPage" value="100"/> + </actionGroup> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="navigateToConfigProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <!--Create visual swatch attribute--> + <actionGroup ref="AddVisualSwatchWithProductWithStorefrontPreviewImageConfigActionGroup" stepKey="addSwatchToProduct"> + <argument name="attribute" value="visualSwatchAttribute"/> + <argument name="option1" value="visualSwatchOption1"/> + <argument name="option2" value="visualSwatchOption2"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickEditConfigurations"/> + <see userInput="Select Attributes" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <!--Add images to product attribute options--> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="MagentoLogo"/> + <argument name="frontend_label" value="{{visualSwatchAttribute.default_label}}"/> + <argument name="label" value="{{visualSwatchOption1.default_label}}"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="TestImageNew"/> + <argument name="frontend_label" value="{{visualSwatchAttribute.default_label}}"/> + <argument name="label" value="{{visualSwatchOption2.default_label}}"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnGenerateProductsButton"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Select any option in the Layered navigation and verify product image--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{visualSwatchAttribute.default_label}}"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption1.default_label)}}" stepKey="waitForOption"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption1.default_label)}}" stepKey="clickFirstOption"/> + <grabAttributeFrom selector="{{StorefrontCategoryMainSection.productImage}}" userInput="src" stepKey="grabFirstOptionImg"/> + <assertContains expectedType="string" expected="{{MagentoLogo.filename}}" actualType="variable" actual="$grabFirstOptionImg" stepKey="assertProductFirstOptionImage"/> + <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeSideBarFilter"/> + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeForSecondOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{visualSwatchAttribute.default_label}}"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption2.default_label)}}" stepKey="waitForSecondOption"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption2.default_label)}}" stepKey="clickSecondOption"/> + <grabAttributeFrom selector="{{StorefrontCategoryMainSection.productImage}}" userInput="src" stepKey="grabSecondOptionImg"/> + <assertContains expectedType="string" expected="{{TestImageNew.filename}}" actualType="variable" actual="$grabSecondOptionImg" stepKey="assertProductSecondOptionImage"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml index 7ef030ef8dfa8..5e712ebc38292 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -26,7 +26,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Swatches/Test/Unit/Block/Adminhtml/Attribute/Edit/Options/TextTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Adminhtml/Attribute/Edit/Options/TextTest.php new file mode 100644 index 0000000000000..e72ebdd4507f4 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Unit/Block/Adminhtml/Attribute/Edit/Options/TextTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Swatches\Test\Unit\Block\Adminhtml\Attribute\Edit\Options; + +use Magento\Swatches\Block\Adminhtml\Attribute\Edit\Options\Text; + +/** + * Class \Magento\Swatches\Test\Unit\Block\Adminhtml\Attribute\Edit\Options\TextTest + */ +class TextTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Text + */ + private $model; + + /** + * Setup environment for test + */ + protected function setUp() + { + $this->model = $this->getMockBuilder(Text::class) + ->disableOriginalConstructor() + ->setMethods(['getReadOnly', 'canManageOptionDefaultOnly', 'getOptionValues']) + ->getMock(); + } + + /** + * Test getJsonConfig with getReadOnly() is true and canManageOptionDefaultOnly() is false + */ + public function testGetJsonConfigDataSet1() + { + $testCase1 = [ + 'dataSet' => [ + 'read_only' => true, + 'can_manage_option_default_only' => false, + 'option_values' => [ + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'red']), + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'blue']), + ] + ], + 'expectedResult' => '{"attributesData":[{"value":6,"label":"red"},{"value":6,"label":"blue"}],' . + '"isSortable":0,"isReadOnly":1}' + + ]; + + $this->executeTest($testCase1); + } + + /** + * Test getJsonConfig with getReadOnly() is false and canManageOptionDefaultOnly() is false + */ + public function testGetJsonConfigDataSet2() + { + $testCase2 = [ + 'dataSet' => [ + 'read_only' => false, + 'can_manage_option_default_only' => false, + 'option_values' => [ + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'red']), + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'blue']), + ] + ], + 'expectedResult' => '{"attributesData":[{"value":6,"label":"red"},{"value":6,"label":"blue"}],' . + '"isSortable":1,"isReadOnly":0}' + + ]; + + $this->executeTest($testCase2); + } + + /** + * Execute test for getJsonConfig() function + */ + public function executeTest($testCase) + { + $this->model->expects($this->any())->method('getReadOnly') + ->willReturn($testCase['dataSet']['read_only']); + $this->model->expects($this->any())->method('canManageOptionDefaultOnly') + ->willReturn($testCase['dataSet']['can_manage_option_default_only']); + $this->model->expects($this->any())->method('getOptionValues')->willReturn( + $testCase['dataSet']['option_values'] + ); + + $this->assertEquals($testCase['expectedResult'], $this->model->getJsonConfig()); + } +} diff --git a/app/code/Magento/Swatches/Test/Unit/Block/Adminhtml/Attribute/Edit/Options/VisualTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Adminhtml/Attribute/Edit/Options/VisualTest.php new file mode 100644 index 0000000000000..f78fedea6afb7 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Unit/Block/Adminhtml/Attribute/Edit/Options/VisualTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Swatches\Test\Unit\Block\Adminhtml\Attribute\Edit\Options; + +use Magento\Swatches\Block\Adminhtml\Attribute\Edit\Options\Visual; + +/** + * Class \Magento\Swatches\Test\Unit\Block\Adminhtml\Attribute\Edit\Options\VisualTest + */ +class VisualTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Visual + */ + private $model; + + /** + * Setup environment for test + */ + protected function setUp() + { + $this->model = $this->getMockBuilder(Visual::class) + ->disableOriginalConstructor() + ->setMethods(['getReadOnly', 'canManageOptionDefaultOnly', 'getOptionValues', 'getUrl']) + ->getMock(); + } + + /** + * Test getJsonConfig with getReadOnly() is true and canManageOptionDefaultOnly() is false + */ + public function testGetJsonConfigDataSet1() + { + $testCase1 = [ + 'dataSet' => [ + 'read_only' => true, + 'can_manage_option_default_only' => false, + 'upload_action_url' => 'http://magento.com/admin/swatches/iframe/show', + 'option_values' => [ + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'red']), + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'blue']), + ] + ], + 'expectedResult' => '{"attributesData":[{"value":6,"label":"red"},{"value":6,"label":"blue"}],' . + '"uploadActionUrl":"http:\/\/magento.com\/admin\/swatches\/iframe\/show","isSortable":0,"isReadOnly":1}' + + ]; + + $this->executeTest($testCase1); + } + + /** + * Test getJsonConfig with getReadOnly() is false and canManageOptionDefaultOnly() is false + */ + public function testGetJsonConfigDataSet2() + { + $testCase1 = [ + 'dataSet' => [ + 'read_only' => false, + 'can_manage_option_default_only' => false, + 'upload_action_url' => 'http://magento.com/admin/swatches/iframe/show', + 'option_values' => [ + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'red']), + new \Magento\Framework\DataObject(['value' => 6, 'label' => 'blue']), + ] + ], + 'expectedResult' => '{"attributesData":[{"value":6,"label":"red"},{"value":6,"label":"blue"}],' . + '"uploadActionUrl":"http:\/\/magento.com\/admin\/swatches\/iframe\/show","isSortable":1,"isReadOnly":0}' + ]; + + $this->executeTest($testCase1); + } + + /** + * Execute test for getJsonConfig() function + */ + public function executeTest($testCase) + { + $this->model->expects($this->any())->method('getReadOnly') + ->willReturn($testCase['dataSet']['read_only']); + $this->model->expects($this->any())->method('canManageOptionDefaultOnly') + ->willReturn($testCase['dataSet']['can_manage_option_default_only']); + $this->model->expects($this->any())->method('getOptionValues')->willReturn( + $testCase['dataSet']['option_values'] + ); + $this->model->expects($this->any())->method('getUrl') + ->willReturn($testCase['dataSet']['upload_action_url']); + + $this->assertEquals($testCase['expectedResult'], $this->model->getJsonConfig()); + } +} diff --git a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php index 317ea77107222..6bdb83c3a8129 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php @@ -6,87 +6,153 @@ namespace Magento\Swatches\Test\Unit\Model\Plugin; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Swatches\Helper\Data; use Magento\Swatches\Model\Plugin\EavAttribute; +use Magento\Swatches\Model\ResourceModel\Swatch\Collection; +use Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory; use Magento\Swatches\Model\Swatch; +use Magento\Swatches\Model\SwatchAttributeType; +use Magento\Swatches\Model\SwatchFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; -class EavAttributeTest extends \PHPUnit\Framework\TestCase +/** + * Test plugin model for Catalog Resource Attribute + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) + */ +class EavAttributeTest extends TestCase { - const ATTRIBUTE_ID = 123; - const OPTION_ID = 'option 12'; - const STORE_ID = 'option 89'; - const ATTRIBUTE_DEFAULT_VALUE = 1; - const ATTRIBUTE_OPTION_VALUE = 2; - const ATTRIBUTE_SWATCH_VALUE = 3; + private const ATTRIBUTE_ID = 123; + private const OPTION_1_ID = 1; + private const OPTION_2_ID = 2; + private const ADMIN_STORE_ID = 0; + private const DEFAULT_STORE_ID = 1; + private const NEW_OPTION_KEY = 'option_2'; + private const ATTRIBUTE_DEFAULT_VALUE = [ + 0 => self::NEW_OPTION_KEY + ]; + private const VISUAL_ATTRIBUTE_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => [ + self::ADMIN_STORE_ID => 'Black', + self::DEFAULT_STORE_ID => 'Black', + ], + self::NEW_OPTION_KEY => [ + self::ADMIN_STORE_ID => 'White', + self::DEFAULT_STORE_ID => 'White', + ], + ] + ]; + private const VISUAL_SWATCH_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => '#000000', + self::NEW_OPTION_KEY => '#ffffff', + ] + ]; + private const VISUAL_SAVED_OPTIONS = [ + [ + 'value' => self::OPTION_1_ID, + 'label' => 'Black', + ], + [ + 'value' => self::OPTION_2_ID, + 'label' => 'White', + ] + ]; + private const TEXT_ATTRIBUTE_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => [ + self::ADMIN_STORE_ID => 'Small', + self::DEFAULT_STORE_ID => 'Small', + ], + self::NEW_OPTION_KEY => [ + self::ADMIN_STORE_ID => 'Medium', + self::DEFAULT_STORE_ID => 'Medium', + ], + ] + ]; + private const TEXT_SWATCH_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => [ + self::ADMIN_STORE_ID => 'S', + self::DEFAULT_STORE_ID => 'S', + ], + self::NEW_OPTION_KEY => [ + self::ADMIN_STORE_ID => 'M', + self::DEFAULT_STORE_ID => 'M', + ], + ] + ]; + private const TEXT_SAVED_OPTIONS = [ + [ + 'value' => self::OPTION_1_ID, + 'label' => 'Small', + ], + [ + 'value' => self::OPTION_2_ID, + 'label' => 'Medium', + ] + ]; /** @var EavAttribute */ private $eavAttribute; - /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Attribute|MockObject */ private $attribute; - /** @var \Magento\Swatches\Model\SwatchFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var SwatchFactory|MockObject */ private $swatchFactory; - /** @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var CollectionFactory|MockObject */ private $collectionFactory; - /** @var \Magento\Swatches\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Data|MockObject */ private $swatchHelper; - /** @var \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource|\PHPUnit_Framework_MockObject_MockObject */ + /** @var AbstractSource|MockObject */ private $abstractSource; - /** @var \Magento\Swatches\Model\Swatch|\PHPUnit_Framework_MockObject_MockObject */ - private $swatch; - - /** @var \Magento\Swatches\Model\ResourceModel\Swatch|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Swatches\Model\ResourceModel\Swatch|MockObject */ private $resource; - /** @var \Magento\Swatches\Model\ResourceModel\Swatch\Collection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Collection|MockObject */ private $collection; - /** @var array */ - private $optionIds = []; - - /** @var array */ - private $allOptions = []; - - /** @var array */ - private $dependencyArray = []; - + /** + * {@inheritDoc} + */ protected function setUp() { - $this->attribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); - $this->swatchFactory = $this->createPartialMock(\Magento\Swatches\Model\SwatchFactory::class, ['create']); - $this->swatchHelper = $this->createMock(\Magento\Swatches\Helper\Data::class); - $this->swatch = $this->createMock(\Magento\Swatches\Model\Swatch::class); - $this->resource = $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch::class); - $this->collection = - $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch\Collection::class); - $this->collectionFactory = $this->createPartialMock( - \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory::class, + $objectManager = new ObjectManager($this); + $this->abstractSource = $this->createMock(AbstractSource::class); + $this->attribute = $this->createPartialMock( + Attribute::class, + ['getSource'] + ); + $this->attribute->setId(self::ATTRIBUTE_ID); + $this->swatchFactory = $this->createPartialMock( + SwatchFactory::class, ['create'] ); - $this->abstractSource = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class); - - $serializer = $this->createPartialMock( - \Magento\Framework\Serialize\Serializer\Json::class, - ['serialize', 'unserialize'] + $this->swatchHelper = $objectManager->getObject( + Data::class, + [ + 'swatchTypeChecker' => $objectManager->getObject(SwatchAttributeType::class) + ] ); - - $serializer->expects($this->any()) - ->method('serialize')->willReturnCallback(function ($parameter) { - return json_encode($parameter); - }); - - $serializer->expects($this->any()) - ->method('unserialize')->willReturnCallback(function ($parameter) { - return json_decode($parameter, true); - }); - - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->resource = $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch::class); + $this->collection = $this->createMock(Collection::class); + $this->collectionFactory = $this->createPartialMock(CollectionFactory::class, ['create']); + $serializer = $objectManager->getObject(Json::class); $this->eavAttribute = $objectManager->getObject( - \Magento\Swatches\Model\Plugin\EavAttribute::class, + EavAttribute::class, [ 'collectionFactory' => $this->collectionFactory, 'swatchFactory' => $this->swatchFactory, @@ -94,220 +160,128 @@ protected function setUp() 'serializer' => $serializer, ] ); - - $this->optionIds = [ - 'value' => ['option 89' => 'test 1', 'option 114' => 'test 2', 'option 170' => 'test 3'], - 'delete' => ['option 89' => 0, 'option 114' => 1, 'option 170' => 0], - ]; - $this->allOptions = [null, ['value' => 'option 12'], ['value' => 'option 154']]; - $this->dependencyArray = ['option 89', 'option 170']; + $this->attribute->expects($this->any()) + ->method('getSource') + ->willReturn($this->abstractSource); + $swatch = $this->createMock(Swatch::class); + $swatch->expects($this->any()) + ->method('getResource') + ->willReturn($this->resource); + $this->swatchFactory->expects($this->any()) + ->method('create') + ->willReturn($swatch); } + /** + * Test beforeSave plugin for visual swatch + */ public function testBeforeSaveVisualSwatch() { - $option = [ - 'value' => [ - 0 => 'option value', + $this->attribute->setData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => self::VISUAL_ATTRIBUTE_OPTIONS, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, ] - ]; - $this->attribute->expects($this->exactly(6))->method('getData')->withConsecutive( - ['defaultvisual'], - ['optionvisual'], - ['swatchvisual'], - ['optionvisual'], - ['option/delete/0'] - )->will($this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $option, - false - )); - - $this->attribute->expects($this->exactly(3))->method('setData') - ->withConsecutive( - ['option', self::ATTRIBUTE_OPTION_VALUE], - ['default', self::ATTRIBUTE_DEFAULT_VALUE], - ['swatch', self::ATTRIBUTE_SWATCH_VALUE] - ); - - $this->swatchHelper->expects($this->once())->method('assembleAdditionalDataEavAttribute') - ->with($this->attribute); - $this->swatchHelper->expects($this->atLeastOnce())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); + ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default')); + $this->assertEquals(self::VISUAL_ATTRIBUTE_OPTIONS, $this->attribute->getData('option')); + $this->assertEquals(self::VISUAL_SWATCH_OPTIONS, $this->attribute->getData('swatch')); } + /** + * Test beforeSave plugin for text swatch + */ public function testBeforeSaveTextSwatch() { - $option = [ - 'value' => [ - 0 => 'option value', + $this->attribute->setData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS, + 'swatchtext' => self::TEXT_SWATCH_OPTIONS, ] - ]; - $this->attribute->expects($this->exactly(6))->method('getData')->withConsecutive( - ['defaulttext'], - ['optiontext'], - ['swatchtext'], - ['optiontext'], - ['option/delete/0'] - )->will( - $this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $option, - false - ) ); - $this->attribute->expects($this->exactly(3))->method('setData') - ->withConsecutive( - ['option', self::ATTRIBUTE_OPTION_VALUE], - ['default', self::ATTRIBUTE_DEFAULT_VALUE], - ['swatch', self::ATTRIBUTE_SWATCH_VALUE] - ); - - $this->swatchHelper->expects($this->once())->method('assembleAdditionalDataEavAttribute') - ->with($this->attribute); - $this->swatchHelper->expects($this->atLeastOnce())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->atLeastOnce())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default')); + $this->assertEquals(self::TEXT_ATTRIBUTE_OPTIONS, $this->attribute->getData('option')); + $this->assertEquals(self::TEXT_SWATCH_OPTIONS, $this->attribute->getData('swatch')); } /** + * Test beforeSave plugin on empty label + * * @expectedException \Magento\Framework\Exception\InputException * @expectedExceptionMessage Admin is a required field in each row */ public function testBeforeSaveWithFailedValidation() { - $optionText = [ - 'value' => [ - 0 => '', + $options = self::VISUAL_ATTRIBUTE_OPTIONS; + $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = ''; + $this->attribute->setData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => $options, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, ] - ]; - $this->swatchHelper->expects($this->once()) - ->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - - $this->swatchHelper->expects($this->atLeastOnce()) - ->method('isVisualSwatch') - ->willReturn(true); - $this->attribute->expects($this->exactly(5)) - ->method('getData') - ->withConsecutive( - ['defaultvisual'], - ['optionvisual'], - ['swatchvisual'], - ['optionvisual'], - ['option/delete/0'] - ) - ->will( - $this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $optionText, - false - ) - ); + ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); $this->eavAttribute->beforeBeforeSave($this->attribute); } /** - * @covers \Magento\Swatches\Model\Plugin\EavAttribute::beforeBeforeSave() + * Test beforeSave plugin on empty label of option being deleted */ - public function testBeforeSaveWithDeletedOption() + public function testValidationIsSkippedForDeletedOption() { - $optionText = [ - 'value' => [ - 0 => '', + $options = self::VISUAL_ATTRIBUTE_OPTIONS; + $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = ''; + $options['delete'][self::NEW_OPTION_KEY] = '1'; + $this->attribute->setData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => $options, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, ] - ]; - - $this->swatchHelper->expects($this->once()) - ->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); + ); - $this->swatchHelper->expects($this->atLeastOnce()) - ->method('isVisualSwatch') - ->willReturn(true); - $this->attribute->expects($this->exactly(6)) - ->method('getData') - ->withConsecutive( - ['defaultvisual'], - ['optionvisual'], - ['swatchvisual'], - ['optionvisual'], - ['option/delete/0'], - ['swatch_input_type'] - ) - ->will( - $this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $optionText, - true, - false - ) - ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default')); + $this->assertEquals($options, $this->attribute->getData('option')); + $this->assertEquals(self::VISUAL_SWATCH_OPTIONS, $this->attribute->getData('swatch')); } + /** + * Test beforeSave plugin for non a swatch attribute + */ public function testBeforeSaveNotSwatch() { $additionalData = [ - 'swatch_input_type' => 'visual', - 'update_product_preview_image' => 1, - 'use_product_image_for_swatch' => 0 - ]; - - $shortAdditionalData = [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL, 'update_product_preview_image' => 1, 'use_product_image_for_swatch' => 0 ]; - $this->attribute->expects($this->exactly(2))->method('getData')->withConsecutive( - [Swatch::SWATCH_INPUT_TYPE_KEY], - ['additional_data'] - )->willReturnOnConsecutiveCalls( - Swatch::SWATCH_INPUT_TYPE_DROPDOWN, - json_encode($additionalData) + $this->attribute->setData( + [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_DROPDOWN, + 'additional_data' => json_encode($additionalData), + ] ); - $this->attribute - ->expects($this->once()) - ->method('setData') - ->with('additional_data', json_encode($shortAdditionalData)) - ->will($this->returnSelf()); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_DROPDOWN); - $this->swatchHelper->expects($this->never())->method('assembleAdditionalDataEavAttribute'); - $this->swatchHelper->expects($this->never())->method('isVisualSwatch'); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->swatchHelper->expects($this->once())->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(false); + unset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY]); - $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(json_encode($additionalData), $this->attribute->getData('additional_data')); } /** @@ -316,390 +290,383 @@ public function testBeforeSaveNotSwatch() public function visualSwatchProvider() { return [ - [Swatch::SWATCH_TYPE_EMPTY, null], - [Swatch::SWATCH_TYPE_VISUAL_COLOR, '#hex'], - [Swatch::SWATCH_TYPE_VISUAL_IMAGE, '/path'], + [Swatch::SWATCH_TYPE_EMPTY, 'black', 'white'], + [Swatch::SWATCH_TYPE_VISUAL_COLOR, '#000000', '#ffffff'], + [Swatch::SWATCH_TYPE_VISUAL_IMAGE, '/path/black.png', '/path/white.png'], ]; } /** - * @dataProvider visualSwatchProvider + * Test afterSave plugin for visual swatch + * + * @param string $swatchType + * @param string $swatch1 + * @param string $swatch2 * - * @param $swatchType - * @param $swatchValue + * @dataProvider visualSwatchProvider */ - public function testAfterAfterSaveVisualSwatch($swatchType, $swatchValue) + public function testAfterAfterSaveVisualSwatch(string $swatchType, string $swatch1, string $swatch2) { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + $options = self::VISUAL_SWATCH_OPTIONS; + $options['value'][self::OPTION_1_ID] = $swatch1; + $options['value'][self::NEW_OPTION_KEY] = $swatch2; + $this->attribute->addData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => self::VISUAL_ATTRIBUTE_OPTIONS, + 'swatchvisual' => $options, + ] + ); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); - $this->swatch->expects($this->once())->method('getId') - ->willReturn(EavAttribute::DEFAULT_STORE_ID); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->exactly(4))->method('setData') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', EavAttribute::DEFAULT_STORE_ID], - ['type', $swatchType], - ['value', $swatchValue] - ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); + $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::VISUAL_SAVED_OPTIONS); + + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') + $this->collection->expects($this->exactly(4)) + ->method('addFieldToFilter') ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', EavAttribute::DEFAULT_STORE_ID] - )->willReturnSelf(); + ['option_id', self::OPTION_1_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID] + ) + ->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') + $this->collection->expects($this->exactly(2)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + $swatchType, + $swatch1, + 1 + ), + $this->createSwatchMock( + $swatchType, + $swatch2, + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(2)) + ->method('create') ->willReturn($this->collection); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => $swatchValue]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchFactory->expects($this->exactly(1))->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); - $this->eavAttribute->afterAfterSave($this->attribute); } - public function testDefaultTextualSwatchAfterSave() + /** + * Test afterSave plugin for text swatch + */ + public function testAfterAfterSaveTextualSwatch() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); + $this->attribute->addData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS, + 'swatchtext' => self::TEXT_SWATCH_OPTIONS, + ] + ); - $this->swatch->expects($this->any())->method('getId') - ->willReturn(EavAttribute::DEFAULT_STORE_ID); - $this->swatch->expects($this->any())->method('save'); - $this->swatch->expects($this->any())->method('isDeleted') - ->with(false); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->collection->expects($this->any())->method('addFieldToFilter') - ->willReturnSelf(); - $this->collection->expects($this->any())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->any())->method('create') - ->willReturn($this->collection); + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::TEXT_SAVED_OPTIONS); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn(null); - - $this->attribute->expects($this->at(3))->method('getData') - ->with('swatch/value') - ->willReturn( - [ - self::STORE_ID => [ - 1 => "test", - 2 => false, - 3 => null, - 4 => "", - ] - ] - ); + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - - $this->swatch->expects($this->any())->method('setData') + $this->collection->expects($this->exactly(8)) + ->method('addFieldToFilter') ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', 1], - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', "test"] - ); - - $this->eavAttribute->afterAfterSave($this->attribute); - } - - public function testAfterAfterSaveTextualSwatch() - { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + ['option_id', self::OPTION_1_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_1_ID], + ['store_id', self::DEFAULT_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::DEFAULT_STORE_ID] + ) + ->willReturnSelf(); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); - $this->swatch->expects($this->once())->method('getId') - ->willReturn(EavAttribute::DEFAULT_STORE_ID); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->once())->method('isDeleted') - ->with(false); - $this->swatch->expects($this->exactly(4))->method('setData') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID], - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', null] + $this->collection->expects($this->exactly(4)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::OPTION_1_ID][self::ADMIN_STORE_ID], + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::OPTION_1_ID][self::DEFAULT_STORE_ID], + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID], + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID], + null, + self::OPTION_2_ID, + self::DEFAULT_STORE_ID + ) ); - - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID] - )->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') + $this->collectionFactory->expects($this->exactly(4)) + ->method('create') ->willReturn($this->collection); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchFactory->expects($this->exactly(1))->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Test afterSave plugin for deleted visual swatch option + */ public function testAfterAfterSaveVisualSwatchIsDelete() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + $options = self::VISUAL_ATTRIBUTE_OPTIONS; + $options['delete'][self::OPTION_1_ID] = '1'; + $this->attribute->addData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => $options, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, + ] + ); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); + $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::VISUAL_SAVED_OPTIONS); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => null]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(true); - - $this->swatchFactory->expects($this->once())->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); + + $this->collection->expects($this->exactly(2)) + ->method('addFieldToFilter') + ->withConsecutive( + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID] + ) + ->willReturnSelf(); + + $this->collection->expects($this->exactly(1)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_VISUAL_COLOR, + self::VISUAL_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY], + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(1)) + ->method('create') + ->willReturn($this->collection); $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Test afterSave plugin for deleted text swatch option + */ public function testAfterAfterSaveTextualSwatchIsDelete() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); - - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); + $options = self::TEXT_ATTRIBUTE_OPTIONS; + $options['delete'][self::OPTION_1_ID] = '1'; + $this->attribute->addData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => $options, + 'swatchtext' => self::TEXT_SWATCH_OPTIONS, + ] + ); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(true); - - $this->swatchFactory->expects($this->once())->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->eavAttribute->afterAfterSave($this->attribute); - } + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::TEXT_SAVED_OPTIONS); - public function testAfterAfterSaveIsSwatchExists() - { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); - $this->swatch->expects($this->once())->method('getId') - ->willReturn(1); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->once())->method('isDeleted') - ->with(false); - $this->swatch->expects($this->exactly(2))->method('setData') + $this->collection->expects($this->exactly(4)) + ->method('addFieldToFilter') ->withConsecutive( - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', null] - ); + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::DEFAULT_STORE_ID] + ) + ->willReturnSelf(); - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID] - )->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') + $this->collection->expects($this->exactly(2)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID], + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID], + null, + self::OPTION_2_ID, + self::DEFAULT_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(2)) + ->method('create') ->willReturn($this->collection); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchFactory->expects($this->exactly(1))->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Test afterSave plugin on empty swatch value + */ public function testAfterAfterSaveNotSwatchAttribute() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - - $this->swatch->expects($this->once())->method('getId') - ->willReturn(1); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->once())->method('isDeleted') - ->with(false); - $this->swatch->expects($this->exactly(2))->method('setData') - ->withConsecutive( - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', null] - ); + $options = self::TEXT_SWATCH_OPTIONS; + $options['value'][self::OPTION_1_ID][self::ADMIN_STORE_ID] = null; + $options['value'][self::OPTION_1_ID][self::DEFAULT_STORE_ID] = null; + $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = null; + $options['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID] = null; + $this->attribute->addData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS, + 'swatchtext' => $options, + ] + ); + + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::TEXT_SAVED_OPTIONS); + + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); + + $this->collection->expects($this->exactly(8)) + ->method('addFieldToFilter') ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID] - )->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') - ->willReturn($this->collection); + ['option_id', self::OPTION_1_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_1_ID], + ['store_id', self::DEFAULT_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::DEFAULT_STORE_ID] + ) + ->willReturnSelf(); - $this->attribute->expects($this->at(0))->method('getData') - ->with('option') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(3))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->will($this->onConsecutiveCalls(true, false)); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); + $this->collection->expects($this->exactly(4)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + null, + self::OPTION_2_ID, + self::DEFAULT_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(4)) + ->method('create') + ->willReturn($this->collection); $this->eavAttribute->afterAfterSave($this->attribute); } + + /** + * Create configured mock for swatch model + * + * @param string $type + * @param string|null $value + * @param int|null $id + * @param int|null $optionId + * @param int|null $storeId + * @return MockObject + */ + private function createSwatchMock( + string $type, + ?string $value, + ?int $id = null, + ?int $optionId = null, + ?int $storeId = null + ) { + $swatch = $this->createMock(Swatch::class); + $swatch->expects($this->any()) + ->method('getId') + ->willReturn($id); + $swatch->expects($this->any()) + ->method('getResource') + ->willReturn($this->resource); + $swatch->expects($this->once()) + ->method('save'); + if ($id) { + $swatch->expects($this->exactly(2)) + ->method('setData') + ->withConsecutive( + ['type', $type], + ['value', $value] + ); + } else { + $swatch->expects($this->exactly(4)) + ->method('setData') + ->withConsecutive( + ['option_id', $optionId], + ['store_id', $storeId], + ['type', $type], + ['value', $value] + ); + } + return $swatch; + } } diff --git a/app/code/Magento/Swatches/Test/Unit/Observer/AddFieldsToAttributeObserverTest.php b/app/code/Magento/Swatches/Test/Unit/Observer/AddFieldsToAttributeObserverTest.php index f8ba5c20250ad..45c680366264b 100644 --- a/app/code/Magento/Swatches/Test/Unit/Observer/AddFieldsToAttributeObserverTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Observer/AddFieldsToAttributeObserverTest.php @@ -10,7 +10,7 @@ */ class AddFieldsToAttributeObserverTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManagerMock; /** @var \Magento\Config\Model\Config\Source\Yesno|\PHPUnit_Framework_MockObject_MockObject */ diff --git a/app/code/Magento/Swatches/Test/Unit/Observer/AddSwatchAttributeTypeObserverTest.php b/app/code/Magento/Swatches/Test/Unit/Observer/AddSwatchAttributeTypeObserverTest.php index 24afa1045e5cb..f78797d93cb0d 100644 --- a/app/code/Magento/Swatches/Test/Unit/Observer/AddSwatchAttributeTypeObserverTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Observer/AddSwatchAttributeTypeObserverTest.php @@ -10,7 +10,7 @@ */ class AddSwatchAttributeTypeObserverTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManagerMock; /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject */ diff --git a/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php new file mode 100644 index 0000000000000..849d79cc58d92 --- /dev/null +++ b/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); +namespace Magento\Swatches\ViewModel\Product\Renderer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Class Configurable + */ +class Configurable implements ArgumentInterface +{ + /** + * Config path if swatch tooltips are enabled + */ + private const XML_PATH_SHOW_SWATCH_TOOLTIP = 'catalog/frontend/show_swatch_tooltip'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Configurable constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get config if swatch tooltips should be rendered. + * + * @return string + */ + public function getShowSwatchTooltip() + { + return $this->scopeConfig->getValue( + self::XML_PATH_SHOW_SWATCH_TOOLTIP, + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Swatches/etc/adminhtml/system.xml b/app/code/Magento/Swatches/etc/adminhtml/system.xml index 2cf40ae83cc3b..6fbf110fadcd3 100644 --- a/app/code/Magento/Swatches/etc/adminhtml/system.xml +++ b/app/code/Magento/Swatches/etc/adminhtml/system.xml @@ -17,6 +17,10 @@ <label>Show Swatches in Product List</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="show_swatch_tooltip" translate="label" type="select" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Show Swatch Tooltip</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> </section> </system> diff --git a/app/code/Magento/Swatches/etc/config.xml b/app/code/Magento/Swatches/etc/config.xml index 65b36558c2796..9d36d9692b295 100644 --- a/app/code/Magento/Swatches/etc/config.xml +++ b/app/code/Magento/Swatches/etc/config.xml @@ -11,6 +11,7 @@ <frontend> <swatches_per_product>16</swatches_per_product> <show_swatches_in_product_list>1</show_swatches_in_product_list> + <show_swatch_tooltip>1</show_swatch_tooltip> </frontend> </catalog> <general> @@ -21,10 +22,5 @@ </input_types> </validator_data> </general> - <design> - <watermark> - <swatch_image_position>stretch</swatch_image_position> - </watermark> - </design> </default> </config> diff --git a/app/code/Magento/Swatches/etc/di.xml b/app/code/Magento/Swatches/etc/di.xml index 5292bfafb6a0f..593786b993560 100644 --- a/app/code/Magento/Swatches/etc/di.xml +++ b/app/code/Magento/Swatches/etc/di.xml @@ -40,39 +40,6 @@ </argument> </arguments> </type> - <type name="Magento\Theme\Model\Design\Config\MetadataProvider"> - <arguments> - <argument name="metadata" xsi:type="array"> - <item name="watermark_swatch_image_size" xsi:type="array"> - <item name="path" xsi:type="string">design/watermark/swatch_image_size</item> - <item name="fieldset" xsi:type="string">other_settings/watermark/swatch_image</item> - </item> - <item name="watermark_swatch_image_imageOpacity" xsi:type="array"> - <item name="path" xsi:type="string">design/watermark/swatch_image_imageOpacity</item> - <item name="fieldset" xsi:type="string">other_settings/watermark/swatch_image</item> - </item> - <item name="watermark_swatch_image_image" xsi:type="array"> - <item name="path" xsi:type="string">design/watermark/swatch_image_image</item> - <item name="fieldset" xsi:type="string">other_settings/watermark/swatch_image</item> - <item name="backend_model" xsi:type="string">Magento\Theme\Model\Design\Backend\Image</item> - <item name="upload_dir" xsi:type="array"> - <item name="config" xsi:type="string">system/filesystem/media</item> - <item name="scope_info" xsi:type="string">1</item> - <item name="value" xsi:type="string">catalog/product/watermark</item> - </item> - <item name="base_url" xsi:type="array"> - <item name="type" xsi:type="string">media</item> - <item name="scope_info" xsi:type="string">1</item> - <item name="value" xsi:type="string">catalog/product/watermark</item> - </item> - </item> - <item name="watermark_swatch_image_position" xsi:type="array"> - <item name="path" xsi:type="string">design/watermark/swatch_image_position</item> - <item name="fieldset" xsi:type="string">other_settings/watermark/swatch_image</item> - </item> - </argument> - </arguments> - </type> <type name="Magento\Swatches\Model\SwatchAttributeCodes"> <arguments> <argument name="cacheKey" xsi:type="string">swatch-attribute-list</argument> @@ -81,4 +48,16 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Attribute\OptionManagement"> + <plugin name="swatches_product_attribute_optionmanagement_plugin" type="Magento\Swatches\Plugin\Eav\Model\Entity\Attribute\OptionManagement"/> + </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="swatch_image" xsi:type="string">catalog_product</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml deleted file mode 100644 index b38e8ecc6e201..0000000000000 --- a/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml +++ /dev/null @@ -1,72 +0,0 @@ -<?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"> - <fieldset name="other_settings"> - <fieldset name="watermark"> - <fieldset name="swatch_image" sortOrder="40"> - <settings> - <level>2</level> - <label translate="true">Swatch Image</label> - </settings> - <field name="watermark_swatch_image_image" formElement="imageUploader"> - <settings> - <label translate="true">Image</label> - <componentType>imageUploader</componentType> - </settings> - <formElements> - <imageUploader> - <settings> - <allowedExtensions>jpg jpeg gif png</allowedExtensions> - <maxFileSize>2097152</maxFileSize> - <uploaderConfig> - <param xsi:type="string" name="url">theme/design_config_fileUploader/save</param> - </uploaderConfig> - </settings> - </imageUploader> - </formElements> - </field> - <field name="watermark_swatch_image_imageOpacity" formElement="input"> - <settings> - <validation> - <rule name="validate-number" xsi:type="boolean">true</rule> - </validation> - <dataType>text</dataType> - <addAfter>%</addAfter> - <label translate="true">Image Opacity</label> - <dataScope>watermark_swatch_image_imageOpacity</dataScope> - </settings> - </field> - <field name="watermark_swatch_image_size" component="Magento_Catalog/component/image-size-field" formElement="input"> - <settings> - <notice translate="true">Example format: 200x300.</notice> - <validation> - <rule name="validate-image-size-range" xsi:type="boolean">true</rule> - </validation> - <dataType>text</dataType> - <label translate="true">Image Size</label> - <dataScope>watermark_swatch_image_size</dataScope> - </settings> - </field> - <field name="watermark_swatch_image_position" formElement="select"> - <settings> - <dataType>text</dataType> - <label translate="true">Image Position</label> - <dataScope>watermark_swatch_image_position</dataScope> - </settings> - <formElements> - <select> - <settings> - <options class="Magento\Catalog\Model\Config\Source\Watermark\Position"/> - </settings> - </select> - </formElements> - </field> - </fieldset> - </fieldset> - </fieldset> -</form> diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js similarity index 98% rename from app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js rename to app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js index 99c4aa64084a6..894a4518f4de8 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js @@ -296,6 +296,12 @@ define([ * @private */ _init: function () { + // Don't render the same set of swatches twice + if ($(this.element).attr('data-rendered')) { + return; + } + $(this.element).attr('data-rendered', true); + if (_.isEmpty(this.options.jsonConfig.images)) { this.options.useAjax = true; // creates debounced variant of _LoadProductMedia() @@ -385,7 +391,8 @@ define([ var $widget = this, container = this.element, classes = this.options.classes, - chooseText = this.options.jsonConfig.chooseText; + chooseText = this.options.jsonConfig.chooseText, + showTooltip = this.options.showTooltip; $widget.optionsMap = {}; @@ -452,10 +459,12 @@ define([ }); }); - // Connect Tooltip - container - .find('[option-type="1"], [option-type="2"], [option-type="0"], [option-type="3"]') - .SwatchRendererTooltip(); + if (showTooltip === 1) { + // Connect Tooltip + container + .find('[option-type="1"], [option-type="2"], [option-type="0"], [option-type="3"]') + .SwatchRendererTooltip(); + } // Hide all elements below more button $('.' + classes.moreButton).nextAll().hide(); @@ -754,7 +763,7 @@ define([ $widget.options.jsonConfig.optionPrices ]); - if (checkAdditionalData['update_product_preview_image'] === '1') { + if (parseInt(checkAdditionalData['update_product_preview_image'], 10) === 1) { $widget._loadMedia(); } @@ -837,7 +846,10 @@ define([ */ _Rewind: function (controls) { controls.find('div[option-id], option[option-id]').removeClass('disabled').removeAttr('disabled'); - controls.find('div[option-empty], option[option-empty]').attr('disabled', true).addClass('disabled'); + controls.find('div[option-empty], option[option-empty]') + .attr('disabled', true) + .addClass('disabled') + .attr('tabindex', '-1'); }, /** @@ -1262,7 +1274,10 @@ define([ dataMergeStrategy: this.options.gallerySwitchStrategy }); } - gallery.first(); + + if (gallery) { + gallery.first(); + } } else if (justAnImage && justAnImage.img) { context.find('.product-image-photo').attr('src', justAnImage.img); } diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml index c2dc36e83950c..86031a189d798 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml @@ -5,10 +5,19 @@ * 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list" /> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml index 6188f3957a11d..98346d6ae7e67 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml @@ -5,11 +5,18 @@ * 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="product.info.options.configurable" remove="true"/> <referenceBlock name="product.info.options.wrapper"> - <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" as="swatch_options" before="-" /> + <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" + as="swatch_options" before="-"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml index 91798cbd9947f..ce31f588c6c8c 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml @@ -3,10 +3,19 @@ ~ 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.widget.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> -</page> \ No newline at end of file +</page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml index c2dc36e83950c..86031a189d798 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml @@ -5,10 +5,19 @@ * 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list" /> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml index 9285d34efcd4c..86031a189d798 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml @@ -5,10 +5,19 @@ * 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml index 9982eb98d84da..c8159f1a43fe3 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml @@ -5,11 +5,18 @@ * 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="product.info.options.configurable" remove="true"/> <referenceBlock name="product.info.options.wrapper"> - <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" as="swatch_options" before="-" /> + <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" + as="swatch_options" before="-"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml index 777277a15d8cd..5838ba9625c6a 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml @@ -7,6 +7,8 @@ <?php /** @var $block \Magento\Swatches\Block\Product\Renderer\Listing\Configurable */ $productId = $block->getProduct()->getId(); +/** @var \Magento\Swatches\ViewModel\Product\Renderer\Configurable $configurableViewModel */ +$configurableViewModel = $block->getConfigurableViewModel() ?> <div class="swatch-opt-<?= $block->escapeHtmlAttr($productId) ?>" data-role="swatch-option-<?= $block->escapeHtmlAttr($productId) ?>"></div> @@ -22,7 +24,8 @@ $productId = $block->getProduct()->getId(); "jsonConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, "jsonSwatchConfig": <?= /* @noEscape */ $block->getJsonSwatchConfig() ?>, "mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>", - "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?> + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>, + "showTooltip": <?= $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?> } } } @@ -39,4 +42,4 @@ $productId = $block->getProduct()->getId(); } } } -</script> \ No newline at end of file +</script> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml index c85a6908413b5..bfabd5f3ab38f 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml @@ -4,7 +4,11 @@ * See COPYING.txt for license details. */ ?> -<?php /** @var $block \Magento\Swatches\Block\Product\Renderer\Configurable */ ?> +<?php +/** @var $block \Magento\Swatches\Block\Product\Renderer\Configurable */ +/** @var \Magento\Swatches\ViewModel\Product\Renderer\Configurable $configurableViewModel */ +$configurableViewModel = $block->getConfigurableViewModel() +?> <div class="swatch-opt" data-role="swatch-options"></div> <script type="text/x-magento-init"> @@ -15,7 +19,8 @@ "jsonSwatchConfig": <?= /* @noEscape */ $swatchOptions = $block->getJsonSwatchConfig() ?>, "mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>", "gallerySwitchStrategy": "<?= $block->escapeJs($block->getVar('gallery_switch_strategy', 'Magento_ConfigurableProduct')) ?: 'replace'; ?>", - "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?> + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>, + "showTooltip": <?= $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?> } }, "*" : { diff --git a/app/code/Magento/SwatchesGraphQl/Plugin/Filters/DataProviderPlugin.php b/app/code/Magento/SwatchesGraphQl/Plugin/Filters/DataProviderPlugin.php index 14c4ba62c8a47..c14ec68b9ab38 100644 --- a/app/code/Magento/SwatchesGraphQl/Plugin/Filters/DataProviderPlugin.php +++ b/app/code/Magento/SwatchesGraphQl/Plugin/Filters/DataProviderPlugin.php @@ -36,6 +36,7 @@ class DataProviderPlugin * * @param FiltersProvider $filtersProvider * @param \Magento\Swatches\Helper\Data $swatchHelper + * @param \Magento\Swatches\Block\LayeredNavigation\RenderLayered $renderLayered */ public function __construct( FiltersProvider $filtersProvider, @@ -53,12 +54,19 @@ public function __construct( * @param Filters $subject * @param \Closure $proceed * @param string $layerType + * @param array|null $attributesToFilter * @return array + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ - public function aroundGetData(Filters $subject, \Closure $proceed, string $layerType) : array - { + public function aroundGetData( + Filters $subject, + \Closure $proceed, + string $layerType, + $attributesToFilter = null + ) : array { $swatchFilters = []; /** @var AbstractFilter $filter */ foreach ($this->filtersProvider->getFilters($layerType) as $filter) { @@ -69,7 +77,7 @@ public function aroundGetData(Filters $subject, \Closure $proceed, string $layer } } - $filtersData = $proceed($layerType); + $filtersData = $proceed($layerType, $attributesToFilter); foreach ($filtersData as $groupKey => $filterGroup) { /** @var AbstractFilter $swatchFilter */ @@ -92,4 +100,5 @@ public function aroundGetData(Filters $subject, \Closure $proceed, string $layer return $filtersData; } + //phpcs:enable } diff --git a/app/code/Magento/Tax/Block/Sales/Order/Tax.php b/app/code/Magento/Tax/Block/Sales/Order/Tax.php index 0adaec9311ee6..9c992185b4e1d 100644 --- a/app/code/Magento/Tax/Block/Sales/Order/Tax.php +++ b/app/code/Magento/Tax/Block/Sales/Order/Tax.php @@ -106,8 +106,8 @@ protected function _addTax($after = 'discount') { $taxTotal = new \Magento\Framework\DataObject(['code' => 'tax', 'block_name' => $this->getNameInLayout()]); $totals = $this->getParentBlock()->getTotals(); - if ($totals['grand_total']) { - $this->getParentBlock()->addTotalBefore($taxTotal, 'grand_total'); + if (isset($totals['grand_total_incl'])) { + $this->getParentBlock()->addTotal($taxTotal, 'grand_total'); } $this->getParentBlock()->addTotal($taxTotal, $after); return $this; @@ -214,6 +214,7 @@ protected function _initSubtotal() protected function _initShipping() { $store = $this->getStore(); + /** @var \Magento\Sales\Block\Order\Totals $parent */ $parent = $this->getParentBlock(); $shipping = $parent->getTotal('shipping'); if (!$shipping) { @@ -232,12 +233,14 @@ protected function _initShipping() $baseShippingIncl = $baseShipping + (double)$this->_source->getBaseShippingTaxAmount(); } + $couponDescription = $this->getCouponDescription(); + $totalExcl = new \Magento\Framework\DataObject( [ 'code' => 'shipping', 'value' => $shipping, 'base_value' => $baseShipping, - 'label' => __('Shipping & Handling (Excl.Tax)'), + 'label' => __('Shipping & Handling (Excl.Tax)') . $couponDescription, ] ); $totalIncl = new \Magento\Framework\DataObject( @@ -245,7 +248,7 @@ protected function _initShipping() 'code' => 'shipping_incl', 'value' => $shippingIncl, 'base_value' => $baseShippingIncl, - 'label' => __('Shipping & Handling (Incl.Tax)'), + 'label' => __('Shipping & Handling (Incl.Tax)') . $couponDescription, ] ); $parent->addTotal($totalExcl, 'shipping'); @@ -319,8 +322,8 @@ protected function _initGrandTotal() 'label' => __('Grand Total (Incl.Tax)'), ] ); - $parent->addTotal($totalExcl, 'grand_total'); - $parent->addTotal($totalIncl, 'tax'); + $parent->addTotal($totalIncl, 'grand_total'); + $parent->addTotal($totalExcl, 'tax'); $this->_addTax('grand_total'); } return $this; @@ -355,4 +358,25 @@ public function getValueProperties() { return $this->getParentBlock()->getValueProperties(); } + + /** + * Returns additional information about coupon code if it is not displayed in totals. + * + * @return string + */ + private function getCouponDescription(): string + { + $couponDescription = ""; + + /** @var \Magento\Sales\Block\Order\Totals $parent */ + $parent = $this->getParentBlock(); + $couponCode = $parent->getSource() + ->getCouponCode(); + + if ($couponCode && !$parent->getTotal('discount')) { + $couponDescription = " ({$couponCode})"; + } + + return $couponDescription; + } } diff --git a/app/code/Magento/Tax/Model/App/Action/ContextPlugin.php b/app/code/Magento/Tax/Model/App/Action/ContextPlugin.php index bba9bc3f3ebe7..913fa4c46f0ae 100644 --- a/app/code/Magento/Tax/Model/App/Action/ContextPlugin.php +++ b/app/code/Magento/Tax/Model/App/Action/ContextPlugin.php @@ -34,7 +34,7 @@ class ContextPlugin /** * Module manager * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManager; @@ -50,15 +50,15 @@ class ContextPlugin * @param \Magento\Framework\App\Http\Context $httpContext * @param \Magento\Tax\Model\Calculation\Proxy $calculation * @param \Magento\Tax\Helper\Data $taxHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\PageCache\Model\Config $cacheConfig */ public function __construct( \Magento\Customer\Model\Session $customerSession, \Magento\Framework\App\Http\Context $httpContext, - \Magento\Tax\Model\Calculation\Proxy $calculation, + \Magento\Tax\Model\Calculation\Proxy $calculation, //phpcs:ignore Magento2.Classes.DiscouragedDependencies \Magento\Tax\Helper\Data $taxHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\PageCache\Model\Config $cacheConfig ) { $this->customerSession = $customerSession; diff --git a/app/code/Magento/Tax/Model/Config.php b/app/code/Magento/Tax/Model/Config.php index 09212ce90bf58..201158eae25dd 100644 --- a/app/code/Magento/Tax/Model/Config.php +++ b/app/code/Magento/Tax/Model/Config.php @@ -14,11 +14,15 @@ use Magento\Store\Model\Store; /** + * Class to set flags for tax display setting + * * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ class Config { - // tax notifications + /** + * Tax notifications + */ const XML_PATH_TAX_NOTIFICATION_IGNORE_DISCOUNT = 'tax/notification/ignore_discount'; const XML_PATH_TAX_NOTIFICATION_IGNORE_PRICE_DISPLAY = 'tax/notification/ignore_price_display'; @@ -70,7 +74,11 @@ class Config const XML_PATH_DISPLAY_CART_SHIPPING = 'tax/cart_display/shipping'; - /** @deprecated */ + /** + * Tax cart display discount + * + * @deprecated + */ const XML_PATH_DISPLAY_CART_DISCOUNT = 'tax/cart_display/discount'; const XML_PATH_DISPLAY_CART_GRANDTOTAL = 'tax/cart_display/grandtotal'; @@ -88,7 +96,11 @@ class Config const XML_PATH_DISPLAY_SALES_SHIPPING = 'tax/sales_display/shipping'; - /** @deprecated */ + /** + * Tax sales display discount + * + * @deprecated + */ const XML_PATH_DISPLAY_SALES_DISCOUNT = 'tax/sales_display/discount'; const XML_PATH_DISPLAY_SALES_GRANDTOTAL = 'tax/sales_display/grandtotal'; @@ -231,6 +243,7 @@ public function discountTax($store = null) /** * Get taxes/discounts calculation sequence. + * * This sequence depends on "Apply Customer Tax" and "Apply Discount On Prices" configuration options. * * @param null|int|string|Store $store @@ -353,6 +366,8 @@ public function setShippingPriceIncludeTax($flag) } /** + * Return the flag for display sales for cart prices including tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -366,6 +381,8 @@ public function displayCartPricesInclTax($store = null) } /** + * Return the flag for display sales for cart prices excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -379,6 +396,8 @@ public function displayCartPricesExclTax($store = null) } /** + * Return the flag for display sales for cart prices both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -392,6 +411,8 @@ public function displayCartPricesBoth($store = null) } /** + * Return the flag for display sales for cart subtotal including tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -405,6 +426,8 @@ public function displayCartSubtotalInclTax($store = null) } /** + * Return the flag for display sales for cart subtotal excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -418,6 +441,8 @@ public function displayCartSubtotalExclTax($store = null) } /** + * Return the flag for display sales for cart subtotal both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -431,6 +456,8 @@ public function displayCartSubtotalBoth($store = null) } /** + * Return the flag for display sales for cart shipping including tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -444,6 +471,8 @@ public function displayCartShippingInclTax($store = null) } /** + * Return the flag for display sales for cart shipping excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -457,6 +486,8 @@ public function displayCartShippingExclTax($store = null) } /** + * Return the flag for display sales for shipping both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -470,6 +501,8 @@ public function displayCartShippingBoth($store = null) } /** + * Return the flag for display cart discount for including tax + * * @param null|string|bool|int|Store $store * @return bool * @deprecated 100.1.3 @@ -484,6 +517,8 @@ public function displayCartDiscountInclTax($store = null) } /** + * Return the flag for display cart discount for excluding tax + * * @param null|string|bool|int|Store $store * @return bool * @deprecated 100.1.3 @@ -498,6 +533,8 @@ public function displayCartDiscountExclTax($store = null) } /** + * Return the flag for display cart discount for both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool * @deprecated 100.1.3 @@ -512,6 +549,8 @@ public function displayCartDiscountBoth($store = null) } /** + * Return the flag for display cart tax with grand total for both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -525,6 +564,8 @@ public function displayCartTaxWithGrandTotal($store = null) } /** + * Return the flag for display cart full summary + * * @param null|string|bool|int|Store $store * @return bool */ @@ -538,6 +579,8 @@ public function displayCartFullSummary($store = null) } /** + * Return the flag for display cart zero tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -551,6 +594,8 @@ public function displayCartZeroTax($store = null) } /** + * Return the flag for display sales prices for including tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -564,6 +609,8 @@ public function displaySalesPricesInclTax($store = null) } /** + * Return the flag for display sales prices for excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -577,6 +624,8 @@ public function displaySalesPricesExclTax($store = null) } /** + * Return the flag for display sales prices for both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -590,6 +639,8 @@ public function displaySalesPricesBoth($store = null) } /** + * Return the flag for display sales subtotal for including tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -603,6 +654,8 @@ public function displaySalesSubtotalInclTax($store = null) } /** + * Return the flag for display sales subtotal for excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -616,6 +669,8 @@ public function displaySalesSubtotalExclTax($store = null) } /** + * Return the flag for display sales subtotal for both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -629,6 +684,8 @@ public function displaySalesSubtotalBoth($store = null) } /** + * Return the flag for display sales for shipping including tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -642,6 +699,8 @@ public function displaySalesShippingInclTax($store = null) } /** + * Return the flag for display sales for shipping excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -655,6 +714,8 @@ public function displaySalesShippingExclTax($store = null) } /** + * Return the flag for display sales for shipping both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -668,6 +729,8 @@ public function displaySalesShippingBoth($store = null) } /** + * Return the flag for display sales discount for including tax + * * @param null|string|bool|int|Store $store * @return bool * @deprecated 100.1.3 @@ -682,6 +745,8 @@ public function displaySalesDiscountInclTax($store = null) } /** + * Return the flag for display sales discount for excluding tax + * * @param null|string|bool|int|Store $store * @return bool * @deprecated 100.1.3 @@ -696,6 +761,8 @@ public function displaySalesDiscountExclTax($store = null) } /** + * Return the flag for display sales discount for both including and excluding tax + * * @param null|string|bool|int|Store $store * @return bool * @deprecated 100.1.3 @@ -710,6 +777,8 @@ public function displaySalesDiscountBoth($store = null) } /** + * Return the flag for display sales tax with grand total + * * @param null|string|bool|int|Store $store * @return bool */ @@ -723,6 +792,8 @@ public function displaySalesTaxWithGrandTotal($store = null) } /** + * Return the flag for display sales full summary + * * @param null|string|bool|int|Store $store * @return bool */ @@ -736,6 +807,8 @@ public function displaySalesFullSummary($store = null) } /** + * Return the flag for display sales zero tax + * * @param null|string|bool|int|Store $store * @return bool */ @@ -829,15 +902,16 @@ public function getInfoUrl($store = null) /** * Check if necessary do product price conversion + * * If it necessary will be returned conversion type (minus or plus) * * @param null|int|string|Store $store - * @return bool|int + * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function needPriceConversion($store = null) { - $res = 0; + $res = false; $priceIncludesTax = $this->priceIncludesTax($store) || $this->getNeedUseShippingExcludeTax(); if ($priceIncludesTax) { switch ($this->getPriceDisplayType($store)) { @@ -845,7 +919,7 @@ public function needPriceConversion($store = null) case self::DISPLAY_TYPE_BOTH: return self::PRICE_CONVERSION_MINUS; case self::DISPLAY_TYPE_INCLUDING_TAX: - $res = false; + $res = $this->displayCartPricesInclTax($store); break; default: break; diff --git a/app/code/Magento/Tax/Model/TaxClassSearchResults.php b/app/code/Magento/Tax/Model/TaxClassSearchResults.php new file mode 100644 index 0000000000000..4e92fd10a3dec --- /dev/null +++ b/app/code/Magento/Tax/Model/TaxClassSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Tax\Api\Data\TaxClassSearchResultsInterface; + +/** + * Service Data Object with Tax Class search results. + */ +class TaxClassSearchResults extends SearchResults implements TaxClassSearchResultsInterface +{ +} diff --git a/app/code/Magento/Tax/Model/TaxRateSearchResults.php b/app/code/Magento/Tax/Model/TaxRateSearchResults.php new file mode 100644 index 0000000000000..80e9b5eaa72fa --- /dev/null +++ b/app/code/Magento/Tax/Model/TaxRateSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Tax\Api\Data\TaxRateSearchResultsInterface; + +/** + * Service Data Object with Tax Rate search results. + */ +class TaxRateSearchResults extends SearchResults implements TaxRateSearchResultsInterface +{ +} diff --git a/app/code/Magento/Tax/Model/TaxRuleSearchResults.php b/app/code/Magento/Tax/Model/TaxRuleSearchResults.php new file mode 100644 index 0000000000000..aa70b31ab22aa --- /dev/null +++ b/app/code/Magento/Tax/Model/TaxRuleSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Tax\Api\Data\TaxRuleSearchResultsInterface; + +/** + * Service Data Object with Tax Rule search results. + */ +class TaxRuleSearchResults extends SearchResults implements TaxRuleSearchResultsInterface +{ +} diff --git a/app/code/Magento/Tax/Observer/AfterAddressSaveObserver.php b/app/code/Magento/Tax/Observer/AfterAddressSaveObserver.php index ef84eac32e95a..cf5d939b35c01 100644 --- a/app/code/Magento/Tax/Observer/AfterAddressSaveObserver.php +++ b/app/code/Magento/Tax/Observer/AfterAddressSaveObserver.php @@ -8,7 +8,7 @@ use Magento\Customer\Model\Address; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\PageCache\Model\Config; use Magento\Tax\Api\TaxAddressManagerInterface; use Magento\Tax\Helper\Data; @@ -26,7 +26,7 @@ class AfterAddressSaveObserver implements ObserverInterface /** * Module manager * - * @var ModuleManagerInterface + * @var Manager */ private $moduleManager; @@ -46,13 +46,13 @@ class AfterAddressSaveObserver implements ObserverInterface /** * @param Data $taxHelper - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param Config $cacheConfig * @param TaxAddressManagerInterface $addressManager */ public function __construct( Data $taxHelper, - ModuleManagerInterface $moduleManager, + Manager $moduleManager, Config $cacheConfig, TaxAddressManagerInterface $addressManager ) { diff --git a/app/code/Magento/Tax/Observer/CustomerLoggedInObserver.php b/app/code/Magento/Tax/Observer/CustomerLoggedInObserver.php index 00b3a9f9e09ad..c1e4ad66d75d7 100644 --- a/app/code/Magento/Tax/Observer/CustomerLoggedInObserver.php +++ b/app/code/Magento/Tax/Observer/CustomerLoggedInObserver.php @@ -9,7 +9,7 @@ use Magento\Customer\Model\Session; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\PageCache\Model\Config; use Magento\Tax\Api\TaxAddressManagerInterface; use Magento\Tax\Helper\Data; @@ -33,7 +33,7 @@ class CustomerLoggedInObserver implements ObserverInterface /** * Module manager * - * @var ModuleManagerInterface + * @var Manager */ private $moduleManager; @@ -60,7 +60,7 @@ class CustomerLoggedInObserver implements ObserverInterface * @param GroupRepositoryInterface $groupRepository * @param Session $customerSession * @param Data $taxHelper - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param Config $cacheConfig * @param TaxAddressManagerInterface $addressManager */ @@ -68,7 +68,7 @@ public function __construct( GroupRepositoryInterface $groupRepository, Session $customerSession, Data $taxHelper, - ModuleManagerInterface $moduleManager, + Manager $moduleManager, Config $cacheConfig, TaxAddressManagerInterface $addressManager ) { diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml index 27c89162b5cea..4b8d79117eb24 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml @@ -26,6 +26,12 @@ <data key="zip">*</data> <data key="rate">8.375</data> </entity> + <entity name="SimpleTaxNYRate" type="tax"> + <data key="state">New York</data> + <data key="country">United States</data> + <data key="zip">*</data> + <data key="rate">20.00</data> + </entity> <entity name="SimpleTaxCA" type="tax"> <data key="state">California</data> <data key="country">United States</data> diff --git a/app/code/Magento/Tax/Test/Unit/App/Action/ContextPluginTest.php b/app/code/Magento/Tax/Test/Unit/App/Action/ContextPluginTest.php index 020baa0c30ec5..a6c7e9bb8685a 100644 --- a/app/code/Magento/Tax/Test/Unit/App/Action/ContextPluginTest.php +++ b/app/code/Magento/Tax/Test/Unit/App/Action/ContextPluginTest.php @@ -38,7 +38,7 @@ class ContextPluginTest extends \PHPUnit\Framework\TestCase /** * Module manager * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManagerMock; @@ -89,7 +89,7 @@ protected function setUp() ) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Tax/Test/Unit/CustomerData/CheckoutTotalsJsLayoutDataProviderTest.php b/app/code/Magento/Tax/Test/Unit/CustomerData/CheckoutTotalsJsLayoutDataProviderTest.php new file mode 100644 index 0000000000000..d624a42c1e134 --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/CustomerData/CheckoutTotalsJsLayoutDataProviderTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Test\Unit\CustomerData; + +use PHPUnit\Framework\TestCase; +use Magento\Tax\CustomerData\CheckoutTotalsJsLayoutDataProvider; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Tax\Model\Config as TaxConfig; + +/** + * Test class to cover CheckoutTotalsJsLayoutDataProvider + * + * Class \Magento\Tax\Test\Unit\CustomerData\CheckoutTotalsJsLayoutDataProviderTest + */ +class CheckoutTotalsJsLayoutDataProviderTest extends TestCase +{ + /** + * @var CheckoutTotalsJsLayoutDataProvider + */ + private $dataProvider; + + /** + * @var TaxConfig|PHPUnit_Framework_MockObject_MockObject + */ + private $taxConfigMock; + + /** + * Setup environment for test + */ + protected function setUp() + { + $this->taxConfigMock = $this->createMock(TaxConfig::class); + $objectManager = new ObjectManagerHelper($this); + + $this->dataProvider = $objectManager->getObject( + CheckoutTotalsJsLayoutDataProvider::class, + [ + 'taxConfig' => $this->taxConfigMock + ] + ); + } + + /** + * Test getData() with dataset getDataDataProvider + * + * @param int $displayCartSubtotalInclTax + * @param int $displayCartSubtotalExclTax + * @param array $expected + * @return void + * @dataProvider getDataDataProvider + */ + public function testGetData($displayCartSubtotalInclTax, $displayCartSubtotalExclTax, $expected) + { + $this->taxConfigMock->expects($this->any())->method('displayCartSubtotalInclTax') + ->willReturn($displayCartSubtotalInclTax); + $this->taxConfigMock->expects($this->any())->method('displayCartSubtotalExclTax') + ->willReturn($displayCartSubtotalExclTax); + + $this->assertEquals($expected, $this->dataProvider->getData()); + } + + /** + * Dataset for test getData() + * + * @return array + */ + public function getDataDataProvider() + { + return [ + 'Test with settings display cart incl and excl is Yes' => [ + '1' , + '1', + [ + 'components' => [ + 'minicart_content' => [ + 'children' => [ + 'subtotal.container' => [ + 'children' => [ + 'subtotal' => [ + 'children' => [ + 'subtotal.totals' => [ + 'config' => [ + 'display_cart_subtotal_incl_tax' => 1, + 'display_cart_subtotal_excl_tax' => 1 + ] + ], + ], + ], + ], + ], + ], + ], + ] + ] + ], + 'Test with settings display cart incl and excl is No' => [ + '0' , + '0', + [ + 'components' => [ + 'minicart_content' => [ + 'children' => [ + 'subtotal.container' => [ + 'children' => [ + 'subtotal' => [ + 'children' => [ + 'subtotal.totals' => [ + 'config' => [ + 'display_cart_subtotal_incl_tax' => 0, + 'display_cart_subtotal_excl_tax' => 0 + ] + ], + ], + ], + ], + ], + ], + ], + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Tax/Test/Unit/Observer/AfterAddressSaveObserverTest.php b/app/code/Magento/Tax/Test/Unit/Observer/AfterAddressSaveObserverTest.php index 2e957e528e294..571cc7173bc92 100644 --- a/app/code/Magento/Tax/Test/Unit/Observer/AfterAddressSaveObserverTest.php +++ b/app/code/Magento/Tax/Test/Unit/Observer/AfterAddressSaveObserverTest.php @@ -6,7 +6,7 @@ namespace Magento\Tax\Test\Unit\Observer; use Magento\Framework\Event\Observer; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\PageCache\Model\Config; use Magento\Tax\Api\TaxAddressManagerInterface; @@ -31,7 +31,7 @@ class AfterAddressSaveObserverTest extends \PHPUnit\Framework\TestCase /** * Module manager * - * @var ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManagerMock; @@ -65,7 +65,7 @@ protected function setUp() ->setMethods(['getCustomerAddress']) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); @@ -77,7 +77,7 @@ protected function setUp() ->setMethods(['isCatalogPriceDisplayAffectedByTax']) ->disableOriginalConstructor() ->getMock(); - + $this->addressManagerMock = $this->getMockBuilder(TaxAddressManagerInterface::class) ->setMethods(['setDefaultAddressAfterSave', 'setDefaultAddressAfterLogIn']) ->disableOriginalConstructor() diff --git a/app/code/Magento/Tax/Test/Unit/Observer/CustomerLoggedInObserverTest.php b/app/code/Magento/Tax/Test/Unit/Observer/CustomerLoggedInObserverTest.php index facbb6733b5c8..c577f1727552f 100644 --- a/app/code/Magento/Tax/Test/Unit/Observer/CustomerLoggedInObserverTest.php +++ b/app/code/Magento/Tax/Test/Unit/Observer/CustomerLoggedInObserverTest.php @@ -31,7 +31,7 @@ class CustomerLoggedInObserverTest extends \PHPUnit\Framework\TestCase /** * Module manager * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManagerMock; @@ -82,7 +82,7 @@ protected function setUp() ) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Tax/etc/db_schema.xml b/app/code/Magento/Tax/etc/db_schema.xml index f5227a9ef3a66..1fe1a1fe33d8a 100644 --- a/app/code/Magento/Tax/etc/db_schema.xml +++ b/app/code/Magento/Tax/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="tax_class" resource="default" engine="innodb" comment="Tax Class"> <column xsi:type="smallint" name="class_id" padding="6" unsigned="false" nullable="false" identity="true" - comment="Class Id"/> + comment="Class ID"/> <column xsi:type="varchar" name="class_name" nullable="false" length="255" comment="Class Name"/> <column xsi:type="varchar" name="class_type" nullable="false" length="8" default="CUSTOMER" comment="Class Type"/> @@ -19,7 +19,7 @@ </table> <table name="tax_calculation_rule" resource="default" engine="innodb" comment="Tax Calculation Rule"> <column xsi:type="int" name="tax_calculation_rule_id" padding="11" unsigned="false" nullable="false" - identity="true" comment="Tax Calculation Rule Id"/> + identity="true" comment="Tax Calculation Rule ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="int" name="priority" padding="11" unsigned="false" nullable="false" identity="false" comment="Priority"/> @@ -40,10 +40,10 @@ </table> <table name="tax_calculation_rate" resource="default" engine="innodb" comment="Tax Calculation Rate"> <column xsi:type="int" name="tax_calculation_rate_id" padding="11" unsigned="false" nullable="false" - identity="true" comment="Tax Calculation Rate Id"/> - <column xsi:type="varchar" name="tax_country_id" nullable="false" length="2" comment="Tax Country Id"/> + identity="true" comment="Tax Calculation Rate ID"/> + <column xsi:type="varchar" name="tax_country_id" nullable="false" length="2" comment="Tax Country ID"/> <column xsi:type="int" name="tax_region_id" padding="11" unsigned="false" nullable="false" identity="false" - comment="Tax Region Id"/> + comment="Tax Region ID"/> <column xsi:type="varchar" name="tax_postcode" nullable="true" length="21" comment="Tax Postcode"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="decimal" name="rate" scale="4" precision="12" unsigned="false" nullable="false" @@ -75,15 +75,15 @@ </table> <table name="tax_calculation" resource="default" engine="innodb" comment="Tax Calculation"> <column xsi:type="int" name="tax_calculation_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Tax Calculation Id"/> + comment="Tax Calculation ID"/> <column xsi:type="int" name="tax_calculation_rate_id" padding="11" unsigned="false" nullable="false" - identity="false" comment="Tax Calculation Rate Id"/> + identity="false" comment="Tax Calculation Rate ID"/> <column xsi:type="int" name="tax_calculation_rule_id" padding="11" unsigned="false" nullable="false" - identity="false" comment="Tax Calculation Rule Id"/> + identity="false" comment="Tax Calculation Rule ID"/> <column xsi:type="smallint" name="customer_tax_class_id" padding="6" unsigned="false" nullable="false" - identity="false" comment="Customer Tax Class Id"/> + identity="false" comment="Customer Tax Class ID"/> <column xsi:type="smallint" name="product_tax_class_id" padding="6" unsigned="false" nullable="false" - identity="false" comment="Product Tax Class Id"/> + identity="false" comment="Product Tax Class ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_id"/> </constraint> @@ -116,11 +116,11 @@ </table> <table name="tax_calculation_rate_title" resource="default" engine="innodb" comment="Tax Calculation Rate Title"> <column xsi:type="int" name="tax_calculation_rate_title_id" padding="11" unsigned="false" nullable="false" - identity="true" comment="Tax Calculation Rate Title Id"/> + identity="true" comment="Tax Calculation Rate Title ID"/> <column xsi:type="int" name="tax_calculation_rate_id" padding="11" unsigned="false" nullable="false" - identity="false" comment="Tax Calculation Rate Id"/> + identity="false" comment="Tax Calculation Rate ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_rate_title_id"/> @@ -140,10 +140,10 @@ </index> </table> <table name="tax_order_aggregated_created" resource="sales" engine="innodb" comment="Tax Order Aggregation"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="float" name="percent" unsigned="false" nullable="true" comment="Percent"/> @@ -170,10 +170,10 @@ </table> <table name="tax_order_aggregated_updated" resource="sales" engine="innodb" comment="Tax Order Aggregated Updated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="float" name="percent" unsigned="false" nullable="true" comment="Percent"/> diff --git a/app/code/Magento/Tax/etc/di.xml b/app/code/Magento/Tax/etc/di.xml index 3b46b0f9e258c..50683b1b879e6 100644 --- a/app/code/Magento/Tax/etc/di.xml +++ b/app/code/Magento/Tax/etc/di.xml @@ -37,8 +37,8 @@ </argument> </arguments> </type> - <preference for="Magento\Tax\Api\Data\TaxRateSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Tax\Api\Data\TaxClassSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Tax\Api\Data\TaxRateSearchResultsInterface" type="Magento\Tax\Model\TaxRateSearchResults" /> + <preference for="Magento\Tax\Api\Data\TaxClassSearchResultsInterface" type="Magento\Tax\Model\TaxClassSearchResults" /> <preference for="Magento\Tax\Api\OrderTaxManagementInterface" type="Magento\Tax\Model\Sales\Order\TaxManagement" /> <preference for="Magento\Tax\Api\Data\OrderTaxDetailsAppliedTaxInterface" type="Magento\Tax\Model\Sales\Order\Tax" /> <preference for="Magento\Tax\Api\Data\OrderTaxDetailsInterface" type="Magento\Tax\Model\Sales\Order\Details" /> @@ -47,7 +47,7 @@ <preference for="Magento\Tax\Api\TaxClassRepositoryInterface" type="Magento\Tax\Model\TaxClass\Repository" /> <preference for="Magento\Tax\Api\Data\TaxClassInterface" type="Magento\Tax\Model\ClassModel" /> <preference for="Magento\Tax\Api\Data\TaxRuleInterface" type="Magento\Tax\Model\Calculation\Rule" /> - <preference for="Magento\Tax\Api\Data\TaxRuleSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Tax\Api\Data\TaxRuleSearchResultsInterface" type="Magento\Tax\Model\TaxRuleSearchResults" /> <preference for="Magento\Tax\Api\TaxRateManagementInterface" type="Magento\Tax\Model\TaxRateManagement" /> <preference for="Magento\Tax\Api\TaxRateRepositoryInterface" type="Magento\Tax\Model\Calculation\RateRepository" /> <preference for="Magento\Tax\Api\Data\TaxRateTitleInterface" type="Magento\Tax\Model\Calculation\Rate\Title" /> @@ -183,4 +183,13 @@ <type name="Magento\Catalog\Ui\DataProvider\Product\Listing\DataProvider"> <plugin name="taxSettingsProvider" type="Magento\Tax\Plugin\Ui\DataProvider\TaxSettings"/> </type> + <type name="Magento\Eav\Model\Config"> + <arguments> + <argument name="attributesForPreload" xsi:type="array"> + <item name="catalog_product" xsi:type="array"> + <item name="tax_class_id" xsi:type="string">catalog_product</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml index 81bdd874ead6c..3558d359aa4d6 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml @@ -10,11 +10,12 @@ require([ 'jquery', 'Magento_Ui/js/modal/alert', + 'Magento_Ui/js/modal/confirm', "jquery/ui", 'mage/multiselect', "mage/mage", 'Magento_Ui/js/modal/modal' -], function($, alert){ +], function($, alert, confirm) { $.widget("adminhtml.dialogRates", $.mage.modal, { options: { @@ -160,53 +161,58 @@ require([ taxRateField.find('.mselect-list') .on('click.mselect-edit', '.mselect-edit', this.edit) .on("click.mselect-delete", ".mselect-delete", function () { - if (!confirm('<?= $block->escapeJs(__('Do you really want to delete this tax rate?')) ?>')) { - return; - } - var that = $(this), select = that.closest('.mselect-list').prev(), rateValue = that.parent().find('input[type="checkbox"]').val(); - $('body').trigger('processStart'); - var ajaxOptions = { - type: 'POST', - data: { - tax_calculation_rate_id: rateValue, - form_key: $('input[name="form_key"]').val() - }, - dataType: 'json', - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateDeleteUrl())) ?>', - success: function(result, status) { - $('body').trigger('processStop'); - if (result.success) { - that.parent().remove(); - select.find('option').each(function() { - if (this.value === rateValue) { - $(this).remove(); + confirm({ + content: '<?= $block->escapeJs(__('Do you really want to delete this tax rate?')) ?>', + actions: { + /** + * Confirm action. + */ + confirm: function () { + $('body').trigger('processStart'); + var ajaxOptions = { + type: 'POST', + data: { + tax_calculation_rate_id: rateValue, + form_key: $('input[name="form_key"]').val() + }, + dataType: 'json', + url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateDeleteUrl())) ?>', + success: function(result, status) { + $('body').trigger('processStop'); + if (result.success) { + that.parent().remove(); + select.find('option').each(function() { + if (this.value === rateValue) { + $(this).remove(); + } + }); + select.trigger('change.hiddenSelect'); + } else { + if (result.error_message) + alert({ + content: result.error_message + }); + else + alert({ + content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + }); + } + }, + error: function () { + $('body').trigger('processStop'); + alert({ + content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + }); } - }); - select.trigger('change.hiddenSelect'); - } else { - if (result.error_message) - alert({ - content: result.error_message - }); - else - alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' - }); + }; + $.ajax(ajaxOptions); } - }, - error: function () { - $('body').trigger('processStop'); - alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' - }); } - }; - $.ajax(ajaxOptions); - + }); }) .on('click.mselectAdd', '.mselect-button-add', function () { taxRateForm diff --git a/app/code/Magento/Tax/view/frontend/layout/sales_email_item_price.xml b/app/code/Magento/Tax/view/frontend/layout/sales_email_item_price.xml index 5dc1e5c72313d..35bcfc265eafc 100644 --- a/app/code/Magento/Tax/view/frontend/layout/sales_email_item_price.xml +++ b/app/code/Magento/Tax/view/frontend/layout/sales_email_item_price.xml @@ -5,8 +5,7 @@ * 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"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="items"> <block class="Magento\Tax\Block\Item\Price\Renderer" name="item_price" template="Magento_Tax::email/items/price/row.phtml"> @@ -16,11 +15,4 @@ </block> </referenceBlock> </body> -</page> - - - - - - - +</page> \ No newline at end of file diff --git a/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/shipping.html b/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/shipping.html index 9f8574a9438d1..269b447cfe66e 100644 --- a/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/shipping.html +++ b/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/shipping.html @@ -9,6 +9,9 @@ <tr class="totals shipping excl"> <th class="mark" scope="row"> <span class="label" data-bind="text: title + ' ' + excludingTaxMessage"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> @@ -19,6 +22,9 @@ <tr class="totals shipping incl"> <th class="mark" scope="row"> <span class="label" data-bind="text: title + ' ' + includingTaxMessage"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> @@ -31,6 +37,9 @@ <tr class="totals shipping incl"> <th class="mark" scope="row"> <span class="label" data-bind="i18n: title"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> @@ -43,6 +52,9 @@ <tr class="totals shipping excl"> <th class="mark" scope="row"> <span class="label" data-bind="i18n: title"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> diff --git a/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/shipping.html b/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/shipping.html index 007e7ded68210..82ab993bb63eb 100644 --- a/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/shipping.html +++ b/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/shipping.html @@ -9,6 +9,9 @@ <tr class="totals shipping excl"> <th class="mark" scope="row"> <span class="label" data-bind="text: title+ ' ' + excludingTaxMessage"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> @@ -25,6 +28,9 @@ <tr class="totals shipping incl"> <th class="mark" scope="row"> <span class="label" data-bind="text: title + ' ' + includingTaxMessage"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> @@ -43,6 +49,9 @@ <tr class="totals shipping incl"> <th class="mark" scope="row"> <span class="label" data-bind="i18n: title"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> @@ -61,6 +70,9 @@ <tr class="totals shipping excl"> <th class="mark" scope="row"> <span class="label" data-bind="i18n: title"></span> + <!-- ko if: haveToShowCoupon() --> + <span class="label description" data-bind="text: getCouponDescription()"></span> + <!-- /ko --> <span class="value" data-bind="text: getShippingMethodTitle()"></span> </th> <td class="amount"> diff --git a/app/code/Magento/TaxGraphQl/etc/schema.graphqls b/app/code/Magento/TaxGraphQl/etc/schema.graphqls index 2b81983478447..d0be08fe9a1bd 100644 --- a/app/code/Magento/TaxGraphQl/etc/schema.graphqls +++ b/app/code/Magento/TaxGraphQl/etc/schema.graphqls @@ -2,5 +2,5 @@ # See COPYING.txt for license details. enum PriceAdjustmentCodesEnum { - TAX + TAX @deprecated(reason: "PriceAdjustmentCodesEnum is deprecated. Tax is included or excluded in price. Tax is not shown separtely in Catalog") } diff --git a/app/code/Magento/TaxImportExport/i18n/en_US.csv b/app/code/Magento/TaxImportExport/i18n/en_US.csv index 95f94dcfd3b2c..56815947ed1fa 100644 --- a/app/code/Magento/TaxImportExport/i18n/en_US.csv +++ b/app/code/Magento/TaxImportExport/i18n/en_US.csv @@ -18,3 +18,5 @@ Rate,Rate CSV,CSV "Excel XML","Excel XML" "Import/Export Tax Rates","Import/Export Tax Rates" +"Please select a file to import!","Please select a file to import!" + diff --git a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml index 7473612252bb2..1c6b267cd9289 100644 --- a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml +++ b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml @@ -31,7 +31,7 @@ </form> <?php endif; ?> <script> -require(['jquery', "mage/mage", "loadingPopup"], function(jQuery){ +require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'mage/translate'], function(jQuery, uiAlert){ jQuery('#import-form').mage('form').mage('validation'); (function ($) { @@ -42,6 +42,10 @@ require(['jquery', "mage/mage", "loadingPopup"], function(jQuery){ }); $(this.form).submit(); + } else { + uiAlert({ + content: $.mage.__('Please select a file to import!') + }); } }); })(jQuery); diff --git a/app/code/Magento/Theme/Block/Html/Header/Logo.php b/app/code/Magento/Theme/Block/Html/Header/Logo.php index b51f624c20339..626a771b4e309 100644 --- a/app/code/Magento/Theme/Block/Html/Header/Logo.php +++ b/app/code/Magento/Theme/Block/Html/Header/Logo.php @@ -98,7 +98,7 @@ public function getLogoWidth() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } - return (int)$this->_data['logo_width'] ? : (int)$this->getLogoImgWidth(); + return (int)$this->_data['logo_width']; } /** @@ -114,7 +114,7 @@ public function getLogoHeight() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } - return (int)$this->_data['logo_height'] ? : (int)$this->getLogoImgHeight(); + return (int)$this->_data['logo_height']; } /** diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php index ad3f4aad676eb..d26c383241f66 100644 --- a/app/code/Magento/Theme/Block/Html/Pager.php +++ b/app/code/Magento/Theme/Block/Html/Pager.php @@ -236,7 +236,7 @@ public function setShowPerPage($varName) */ public function isShowPerPage() { - if (sizeof($this->getAvailableLimit()) <= 1) { + if (count($this->getAvailableLimit()) <= 1) { return false; } return $this->_showPerPage; diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index 77b9144069502..fd8aaa7708cf3 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -133,7 +133,7 @@ protected function _countItems($items) * * @param Menu $items * @param int $limit - * @return array + * @return array|void * * @todo: Add Depth Level limit, and better logic for columns */ @@ -141,7 +141,7 @@ protected function _columnBrake($items, $limit) { $total = $this->_countItems($items); if ($total <= $limit) { - return []; + return; } $result[] = ['total' => $total, 'max' => (int)ceil($total / ceil($total / $limit))]; diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index a9aaaedb726d6..8f81ace8c9047 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -247,7 +247,7 @@ private function getMime() */ private function getRelativeMediaPath(string $path): string { - return preg_replace('/\/(pub\/)?media\//', '', $path); + return preg_split('/\/(pub\/)?media\//', $path)[1] ?? preg_replace('/\/(pub\/)?media\//', '', $path); } /** diff --git a/app/code/Magento/Theme/Model/Design/BackendModelFactory.php b/app/code/Magento/Theme/Model/Design/BackendModelFactory.php index 155afa89c4173..df4ad381ca0d8 100644 --- a/app/code/Magento/Theme/Model/Design/BackendModelFactory.php +++ b/app/code/Magento/Theme/Model/Design/BackendModelFactory.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Theme\Model\Design; use Magento\Framework\App\Config\Value; @@ -11,6 +14,9 @@ use Magento\Theme\Model\Design\Config\MetadataProvider; use Magento\Theme\Model\ResourceModel\Design\Config\CollectionFactory; +/** + * Class BackendModelFactory + */ class BackendModelFactory extends ValueFactory { /** @@ -58,13 +64,15 @@ public function __construct( */ public function create(array $data = []) { + $storedData = $this->getStoredData($data['scope'], $data['scopeId'], $data['config']['path']); + $backendModelData = array_replace_recursive( - $this->getStoredData($data['scope'], $data['scopeId'], $data['config']['path']), + $storedData, [ 'path' => $data['config']['path'], 'scope' => $data['scope'], 'scope_id' => $data['scopeId'], - 'field_config' => $data['config'], + 'field_config' => $data['config'] ] ); @@ -76,6 +84,13 @@ public function create(array $data = []) $backendModel = $this->getNewBackendModel($backendType, $backendModelData); $backendModel->setValue($data['value']); + if ($storedData) { + foreach ($storedData as $key => $value) { + $backendModel->setOrigData($key, $value); + } + $backendModel->setOrigData('field_config', $data['config']); + } + return $backendModel; } @@ -166,9 +181,12 @@ protected function getMetadata() { if (!$this->metadata) { $this->metadata = $this->metadataProvider->get(); - array_walk($this->metadata, function (&$value) { - $value = $value['path']; - }); + array_walk( + $this->metadata, + function (&$value) { + $value = $value['path']; + } + ); } return $this->metadata; } diff --git a/app/code/Magento/Theme/Model/Design/Config/ValueChecker.php b/app/code/Magento/Theme/Model/Design/Config/ValueChecker.php index 42801f57c8822..11d45616e387d 100644 --- a/app/code/Magento/Theme/Model/Design/Config/ValueChecker.php +++ b/app/code/Magento/Theme/Model/Design/Config/ValueChecker.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Config as AppConfig; use Magento\Framework\App\ScopeFallbackResolverInterface; +/** + * Class ValueChecker + */ class ValueChecker { /** @@ -61,7 +64,7 @@ public function isDifferentFromDefault($value, $scope, $scopeId, array $fieldCon $fieldConfig ), $this->valueProcessor->process( - $this->appConfig->getValue($fieldConfig['path'], $scope, $scopeId), + ($this->appConfig->getValue($fieldConfig['path'], $scope, $scopeId) ?? ""), $scope, $scopeId, $fieldConfig @@ -80,12 +83,11 @@ public function isDifferentFromDefault($value, $scope, $scopeId, array $fieldCon */ protected function isEqual($value, $defaultValue) { - switch (gettype($value)) { - case 'array': - return $this->isEqualArrays($value, $defaultValue); - default: - return $value === $defaultValue; + if (is_array($value)) { + return $this->isEqualArrays($value, $defaultValue); } + + return $value === $defaultValue; } /** diff --git a/app/code/Magento/Theme/Model/Theme/ThemeProvider.php b/app/code/Magento/Theme/Model/Theme/ThemeProvider.php index cd3f7f8a2dc0e..04e4c131dbcd3 100644 --- a/app/code/Magento/Theme/Model/Theme/ThemeProvider.php +++ b/app/code/Magento/Theme/Model/Theme/ThemeProvider.php @@ -93,8 +93,8 @@ public function getThemeByFullPath($fullPath) if ($theme->getId()) { $this->saveThemeToCache($theme, 'theme' . $fullPath); $this->saveThemeToCache($theme, 'theme-by-id-' . $theme->getId()); - $this->themes[$fullPath] = $theme; } + $this->themes[$fullPath] = $theme; return $theme; } @@ -167,6 +167,8 @@ private function saveThemeToCache(\Magento\Theme\Model\Theme $theme, $cacheId) } /** + * Get theme list + * * @deprecated 100.1.3 * @return ListInterface */ @@ -179,6 +181,8 @@ private function getThemeList() } /** + * Get deployment config + * * @deprecated 100.1.3 * @return DeploymentConfig */ diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/AdminChangeStorefrontThemeActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/AdminChangeStorefrontThemeActionGroup.xml new file mode 100644 index 0000000000000..0620b9b73ba96 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/AdminChangeStorefrontThemeActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminChangeStorefrontThemeActionGroup"> + <arguments> + <argument name="theme" type="string"/> + <argument name="scopeColumn" type="string" defaultValue="Store View"/> + <argument name="scopeName" type="string" defaultValue="{{_defaultStore.name}}"/> + </arguments> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage"/> + <click selector="{{AdminDesignConfigSection.scopeEditLinkByName(scopeColumn, scopeName)}}" stepKey="editScopeConfig"/> + <selectOption selector="{{AdminDesignConfigSection.appliedTheme}}" userInput="{{theme}}" stepKey="selectTheme"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontCheckElementColorActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontCheckElementColorActionGroup.xml new file mode 100644 index 0000000000000..66e98d5e41527 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontCheckElementColorActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontCheckElementColorActionGroup"> + <annotations> + <description>Checks element color on storefront.</description> + </annotations> + <arguments> + <argument name="selector" type="string"/> + <argument name="property" type="string"/> + <argument name="color" type="string"/> + </arguments> + + <executeJS function="return window.getComputedStyle(document.querySelector('{{selector}}')).getPropertyValue('{{property}}')" stepKey="getElementColor"/> + <assertEquals expected="{{color}}" expectedType="string" actualType="variable" actual="getElementColor" message="pass" stepKey="assertElementColor"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml b/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml index ec28e8ed7a999..1a3c10745f5a7 100644 --- a/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml +++ b/app/code/Magento/Theme/Test/Mftf/Data/DesignData.xml @@ -11,4 +11,10 @@ <entity name="Layout" type="page_layout"> <data key="1column">1 column</data> </entity> + <entity name="MagentoBlankTheme" type="theme"> + <data key="name">Magento Blank</data> + </entity> + <entity name="MagentoLumaTheme" type="theme"> + <data key="name">Magento Luma</data> + </entity> </entities> diff --git a/app/code/Magento/Theme/Test/Mftf/Data/NavigationMenuColorData.xml b/app/code/Magento/Theme/Test/Mftf/Data/NavigationMenuColorData.xml new file mode 100644 index 0000000000000..7af07753d7c9c --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Data/NavigationMenuColorData.xml @@ -0,0 +1,16 @@ +<?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="NavigationMenuColor" type="navigation_menu_color"> + <data key="gray">rgb(232, 232, 232)</data> + <data key="white">rgb(255, 255, 255)</data> + <data key="orange">rgb(255, 85, 1)</data> + </entity> +</entities> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml index c2652f33f7606..762537ba426f2 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml @@ -31,5 +31,8 @@ <element name="storesArrow" type="button" selector="#ZmF2aWNvbi9zdG9yZXM- > .jstree-icon" /> <element name="checkIfStoresArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbi9zdG9yZXM-' and contains(@class,'jstree-closed')]" /> <element name="storeLink" type="button" selector="#ZmF2aWNvbi9zdG9yZXMvMQ-- > a"/> + <element name="imageWatermarkType" type="text" selector="//div[contains(@class, 'fieldset-wrapper-title')]//span[contains(text(), '{{watermarkType}}')]" parameterized="true"/> + <element name="appliedTheme" type="select" selector="select[name='theme_theme_id']"/> + <element name="scopeEditLinkByName" type="button" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[normalize-space(.)= '{{scope}}']/preceding-sibling::th)+1][contains(.,'{{scopeName}}')]/..//a[contains(@class, 'action-menu-item')]" timeout="30" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/AdminThemeSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminThemeSection.xml index 219ca7420361c..1a0bb738bc9ca 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/AdminThemeSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminThemeSection.xml @@ -15,7 +15,8 @@ <element name="rowsInThemeTitleColumn" type="text" selector="//tbody/tr/td[contains(@class, 'parent_theme')]/preceding-sibling::td"/> <element name="rowsInColumn" type="text" selector="//tbody/tr/td[contains(@class, '{{column}}')]" parameterized="true"/> <!--Specific cell e.g. {{Section.gridCell('Name')}}--> - <element name="gridCell" type="text" selector="//table[@id='theme_grid_table']//td[contains(text(), '{{gridCellText}}')]" parameterized="true"/> + <element name="gridCell" type="text" selector="//table//div[contains(text(), '{{gridCellText}}')]" parameterized="true"/> + <element name="gridCellUpdated" type="text" selector="//tbody//tr//div[contains(text(), '{{gridCellText}}')]" parameterized="true"/> <element name="columnHeader" type="text" selector="//thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontNavigationMenuSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontNavigationMenuSection.xml new file mode 100644 index 0000000000000..5741b50f877f6 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontNavigationMenuSection.xml @@ -0,0 +1,21 @@ +<?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="StorefrontNavigationMenuSection"> + <element name="navigationMenu" type="block" selector=".section-items.nav-sections-items li"/> + <element name="subItemLevelHover" type="text" selector=".{{level}} .submenu a:hover" parameterized="true"/> + <element name="itemByNameAndLevel" type="text" selector="//a[span[contains(., '{{itemName}}')]]/following-sibling::ul[contains(@class,'{{itemLevel}}')]" parameterized="true"/> + <element name="subItemByLevel" type="text" selector="li.{{itemLevel}}.parent ul.{{itemLevel}}" parameterized="true"/> + <element name="itemActiveState" type="text" selector=".navigation .level0.active>.level-top"/> + <element name="subItemActiveState" type="text" selector=".navigation .level0 .submenu .active>a"/> + <element name="submenuLeftDirection" type="text" selector="ul.{{itemLevel}}.submenu-reverse" parameterized="true"/> + <element name="submenuRightDirection" type="text" selector="ul.{{itemLevel}}:not(.submenu-reverse)" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml new file mode 100644 index 0000000000000..9facab57e9a09 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml @@ -0,0 +1,39 @@ +<?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="AdminContentThemesSortTest"> + <annotations> + <features value="Theme"/> + <stories value="Menu Navigation"/> + <title value="Admin content themes sort themes test"/> + <description value="Admin should be able to sort Themes"/> + <severity value="CRITICAL"/> + <testCaseId value="https://github.com/magento/magento2/pull/25926"/> + <group value="menu"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentThemesPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentDesignThemes.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentDesignThemes.pageTitle}}"/> + </actionGroup> + <click selector="{{AdminThemeSection.columnHeader('Theme Title')}}" stepKey="clickSortByTitle"/> + <click selector="{{AdminThemeSection.columnHeader('Theme Title')}}" stepKey="clickSortByTitleSecondTime"/> + <seeNumberOfElements selector="{{AdminThemeSection.rowsInColumn('theme_path')}}" userInput="2" stepKey="see2RowsOnTheGrid"/> + <seeNumberOfElements selector="{{AdminThemeSection.gridCellUpdated('Magento Luma')}}" userInput="1" stepKey="seeLumaThemeInTitleColumn"/> + </test> +</tests> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml index a667f40ad327f..afe70243f602c 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml @@ -16,6 +16,9 @@ <description value="Watermark images should be able to be uploaded in the admin"/> <severity value="MAJOR"/> <testCaseId value="MC-5796"/> + <skip> + <issueId value="MC-18496"/> + </skip> <group value="Watermark"/> </annotations> <before> diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php index 91af1a9bf6078..077f12e578dca 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php @@ -44,4 +44,38 @@ public function testGetLogoSrc() $this->assertEquals('http://localhost/pub/media/logo/default/image.gif', $block->getLogoSrc()); } + + /** + * cover \Magento\Theme\Block\Html\Header\Logo::getLogoHeight + */ + public function testGetLogoHeight() + { + $scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $scopeConfig->expects($this->once())->method('getValue')->willReturn(null); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $arguments = [ + 'scopeConfig' => $scopeConfig, + ]; + $block = $objectManager->getObject(\Magento\Theme\Block\Html\Header\Logo::class, $arguments); + + $this->assertEquals(null, $block->getLogoHeight()); + } + + /** + * @covers \Magento\Theme\Block\Html\Header\Logo::getLogoWidth + */ + public function testGetLogoWidth() + { + $scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $scopeConfig->expects($this->once())->method('getValue')->willReturn('170'); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $arguments = [ + 'scopeConfig' => $scopeConfig, + ]; + $block = $objectManager->getObject(\Magento\Theme\Block\Html\Header\Logo::class, $arguments); + + $this->assertEquals('170', $block->getLogoHeight()); + } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php index 94a6ab0ec565e..91f176efbc7b9 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php @@ -282,4 +282,35 @@ public function testBeforeSaveWithExistingFile() $this->fileBackend->getValue() ); } + + /** + * Test for getRelativeMediaPath method. + * + * @param string $path + * @param string $filename + * @dataProvider getRelativeMediaPathDataProvider + */ + public function testGetRelativeMediaPath(string $path, string $filename) + { + $reflection = new \ReflectionClass($this->fileBackend); + $method = $reflection->getMethod('getRelativeMediaPath'); + $method->setAccessible(true); + $this->assertEquals( + $filename, + $method->invoke($this->fileBackend, $path . $filename) + ); + } + + /** + * Data provider for testGetRelativeMediaPath. + * + * @return array + */ + public function getRelativeMediaPathDataProvider(): array + { + return [ + 'Normal path' => ['pub/media/', 'filename.jpg'], + 'Complex path' => ['somepath/pub/media/', 'filename.jpg'], + ]; + } } diff --git a/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/EditActionTest.php b/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/EditActionTest.php index 2ac959d0a9881..22cc1c9e89fbe 100644 --- a/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/EditActionTest.php +++ b/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/EditActionTest.php @@ -9,6 +9,9 @@ use Magento\Store\Model\ScopeInterface; use Magento\Theme\Ui\Component\Listing\Column\EditAction; +/** + * Class EditActionTest + */ class EditActionTest extends \PHPUnit\Framework\TestCase { /** @var EditAction */ @@ -64,6 +67,7 @@ public function testPrepareDataSource($dataSourceItem, $scope, $scopeId) 'edit' => [ 'href' => 'http://magento.com/theme/design_config/edit', 'label' => new \Magento\Framework\Phrase('Edit'), + '__disableTmpl' => true, ] ], ]; diff --git a/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php b/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php index 03d1fe70f2f07..5e2fe51043885 100644 --- a/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php +++ b/app/code/Magento/Theme/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php @@ -99,20 +99,52 @@ public function getPrepareDataSourceDataProvider() { return [ [ - ['name' => 'itemName', 'config' => []], - [['itemName' => '', 'entity_id' => 1]], - [['itemName' => ['view' => ['href' => 'url', 'label' => __('View')]], 'entity_id' => 1]], + [ + 'name' => 'itemName', + 'config' => [] + ], + [ + ['itemName' => '', 'entity_id' => 1] + ], + [ + [ + 'itemName' => [ + 'view' => [ + 'href' => 'url', + 'label' => __('View'), + '__disableTmpl' => true, + ] + ], + 'entity_id' => 1 + ] + ], '#', ['id' => 1] ], [ - ['name' => 'itemName', 'config' => [ - 'viewUrlPath' => 'url_path', - 'urlEntityParamName' => 'theme_id', - 'indexField' => 'theme_id'] + [ + 'name' => 'itemName', + 'config' => [ + 'viewUrlPath' => 'url_path', + 'urlEntityParamName' => 'theme_id', + 'indexField' => 'theme_id' + ] + ], + [ + ['itemName' => '', 'theme_id' => 2] + ], + [ + [ + 'itemName' => [ + 'view' => [ + 'href' => 'url', + 'label' => __('View'), + '__disableTmpl' => true, + ] + ], + 'theme_id' => 2 + ] ], - [['itemName' => '', 'theme_id' => 2]], - [['itemName' => ['view' => ['href' => 'url', 'label' => __('View')]], 'theme_id' => 2]], 'url_path', ['theme_id' => 2] ] diff --git a/app/code/Magento/Theme/Ui/Component/Listing/Column/EditAction.php b/app/code/Magento/Theme/Ui/Component/Listing/Column/EditAction.php index 801f30ce30b0a..1eeeaccff88ce 100644 --- a/app/code/Magento/Theme/Ui/Component/Listing/Column/EditAction.php +++ b/app/code/Magento/Theme/Ui/Component/Listing/Column/EditAction.php @@ -73,7 +73,8 @@ public function prepareDataSource(array $dataSource) 'scope_id' => $scopeId, ] ), - 'label' => __('Edit') + 'label' => __('Edit'), + '__disableTmpl' => true, ] ]; } diff --git a/app/code/Magento/Theme/Ui/Component/Listing/Column/ViewAction.php b/app/code/Magento/Theme/Ui/Component/Listing/Column/ViewAction.php index 774d5bab660af..9e47e2c52bddf 100644 --- a/app/code/Magento/Theme/Ui/Component/Listing/Column/ViewAction.php +++ b/app/code/Magento/Theme/Ui/Component/Listing/Column/ViewAction.php @@ -65,7 +65,8 @@ public function prepareDataSource(array $dataSource) : array $urlEntityParamName => $item[$indexField] ] ), - 'label' => __('View') + 'label' => __('View'), + '__disableTmpl' => true, ] ]; } diff --git a/app/code/Magento/Theme/composer.json b/app/code/Magento/Theme/composer.json index 37802ee6c68f6..ecc944336cd86 100644 --- a/app/code/Magento/Theme/composer.json +++ b/app/code/Magento/Theme/composer.json @@ -19,7 +19,6 @@ "magento/module-widget": "*" }, "suggest": { - "magento/module-translation": "*", "magento/module-theme-sample-data": "*", "magento/module-deploy": "*", "magento/module-directory": "*" diff --git a/app/code/Magento/Theme/etc/db_schema.xml b/app/code/Magento/Theme/etc/db_schema.xml index 7f3a3fc607947..84b7654e69160 100644 --- a/app/code/Magento/Theme/etc/db_schema.xml +++ b/app/code/Magento/Theme/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Theme identifier"/> <column xsi:type="int" name="parent_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="varchar" name="theme_path" nullable="true" length="255" comment="Theme Path"/> <column xsi:type="varchar" name="theme_title" nullable="false" length="255" comment="Theme Title"/> <column xsi:type="varchar" name="preview_image" nullable="true" length="255" comment="Preview Image"/> @@ -28,7 +28,7 @@ <column xsi:type="int" name="theme_files_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Theme files identifier"/> <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Theme Id"/> + comment="Theme ID"/> <column xsi:type="varchar" name="file_path" nullable="true" length="255" comment="Relative path to file"/> <column xsi:type="varchar" name="file_type" nullable="false" length="32" comment="File Type"/> <column xsi:type="longtext" name="content" nullable="false" comment="File Content"/> @@ -43,9 +43,9 @@ </table> <table name="design_change" resource="default" engine="innodb" comment="Design Changes"> <column xsi:type="int" name="design_change_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Design Change Id"/> + comment="Design Change ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="design" nullable="true" length="255" comment="Design"/> <column xsi:type="date" name="date_from" comment="First Date of Design Activity"/> <column xsi:type="date" name="date_to" comment="Last Date of Design Activity"/> diff --git a/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml b/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml index 9cb89746ad85d..cee241233a199 100644 --- a/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml +++ b/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml @@ -31,7 +31,7 @@ </container> <container name="page.main.container" as="page_main_container" htmlId="page:main-container" htmlTag="div" htmlClass="page-columns"> <container name="main.col" as="main-col" htmlId="container" htmlTag="div" htmlClass="main-col"> - <container name="admin.scope.col.wrap" as="admin-scope-col-wrap" htmlTag="div" htmlClass="admin__scope-old"> <!-- ToDo UI: remove this wrapper remove with old styles removal --> + <container name="admin.scope.col.wrap" as="admin-scope-col-wrap" htmlTag="div" htmlClass="form-inline"> <!-- ToDo UI: remove this wrapper remove with old styles removal --> <container name="content" as="content"/> </container> </container> diff --git a/app/code/Magento/Theme/view/adminhtml/ui_component/design_theme_listing.xml b/app/code/Magento/Theme/view/adminhtml/ui_component/design_theme_listing.xml index 14aea72d87357..d2e5fa7ae1ca9 100644 --- a/app/code/Magento/Theme/view/adminhtml/ui_component/design_theme_listing.xml +++ b/app/code/Magento/Theme/view/adminhtml/ui_component/design_theme_listing.xml @@ -20,6 +20,9 @@ <dataSource name="design_theme_listing_data_source" component="Magento_Ui/js/grid/provider"> <settings> <updateUrl path="mui/index/render"/> + <storageConfig> + <param name="indexField" xsi:type="string">theme_id</param> + </storageConfig> </settings> <aclResource>Magento_Theme::theme</aclResource> <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="design_theme_listing_data_source"> diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 07d344cb3658f..8eaac4aa3e794 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -11,8 +11,6 @@ <block name="require.js" class="Magento\Framework\View\Element\Template" template="Magento_Theme::page/js/require_js.phtml" /> <referenceContainer name="after.body.start"> <block class="Magento\RequireJs\Block\Html\Head\Config" name="requirejs-config"/> - <block class="Magento\Translation\Block\Html\Head\Config" name="translate-config"/> - <block class="Magento\Translation\Block\Js" name="translate" template="Magento_Translation::translate.phtml"/> <block class="Magento\Framework\View\Element\Js\Cookie" name="js_cookies" template="Magento_Theme::js/cookie.phtml"/> <block class="Magento\Theme\Block\Html\Notices" name="global_notices" template="Magento_Theme::html/notices.phtml"/> </referenceContainer> @@ -52,12 +50,7 @@ </container> </container> <container name="header-wrapper" label="Page Header" as="header-wrapper" htmlTag="div" htmlClass="header content"> - <block class="Magento\Theme\Block\Html\Header\Logo" name="logo"> - <arguments> - <argument name="logo_img_width" xsi:type="number">189</argument> - <argument name="logo_img_height" xsi:type="number">64</argument> - </arguments> - </block> + <block class="Magento\Theme\Block\Html\Header\Logo" name="logo"/> </container> </referenceContainer> <referenceContainer name="page.top"> diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js index 41cc0d9813bfa..58074216458ee 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js @@ -197,7 +197,7 @@ }, /** - * Wraps the node in in another node. + * Wraps the node in another node. * * @example * node.wrap(wrapperNode); diff --git a/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js b/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js index bb3300baf988a..15bc5465e5d04 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js +++ b/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js @@ -78,6 +78,8 @@ define([ } tinyMCE3.init(this.getSettings(mode)); + varienGlobalEvents.clearEventHandlers('open_browser_callback'); + varienGlobalEvents.attachEventHandler('open_browser_callback', tinyMceEditors.get(this.id).openFileBrowser); }, /** diff --git a/app/code/Magento/Translation/Console/Command/UninstallLanguageCommand.php b/app/code/Magento/Translation/Console/Command/UninstallLanguageCommand.php index a1092b231479e..4f7a1133ab208 100644 --- a/app/code/Magento/Translation/Console/Command/UninstallLanguageCommand.php +++ b/app/code/Magento/Translation/Console/Command/UninstallLanguageCommand.php @@ -85,31 +85,33 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { $this->setName('i18n:uninstall') ->setDescription('Uninstalls language packages') - ->setDefinition([ - new InputArgument( - self::PACKAGE_ARGUMENT, - InputArgument::IS_ARRAY | InputArgument::REQUIRED, - 'Language package name' - ), - new InputOption( - self::BACKUP_CODE_OPTION, - '-b', - InputOption::VALUE_NONE, - 'Take code and configuration files backup (excluding temporary files)' - ), - ]); + ->setDefinition( + [ + new InputArgument( + self::PACKAGE_ARGUMENT, + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'Language package name' + ), + new InputOption( + self::BACKUP_CODE_OPTION, + '-b', + InputOption::VALUE_NONE, + 'Take code and configuration files backup (excluding temporary files)' + ) + ] + ); parent::configure(); } /** - * {@inheritdoc} + * @inheritdoc */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -121,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$this->validate($package)) { $output->writeln("<info>Package $package is not a Magento language and will be skipped.</info>"); } else { - if (sizeof($dependencies[$package]) > 0) { + if (count($dependencies[$package]) > 0) { $output->writeln("<info>Package $package has dependencies and will be skipped.</info>"); } else { $packagesToRemove[] = $package; diff --git a/app/code/Magento/Translation/Model/Inline/Parser.php b/app/code/Magento/Translation/Model/Inline/Parser.php index 52124aee869af..ca66d288d6f60 100644 --- a/app/code/Magento/Translation/Model/Inline/Parser.php +++ b/app/code/Magento/Translation/Model/Inline/Parser.php @@ -16,7 +16,7 @@ use Magento\Translation\Model\Inline\CacheManager; /** - * Parse content, applying necessary html element wrapping and client scripts for inline translation. + * Parses content and applies necessary html element wrapping and client scripts for inline translation. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -229,7 +229,12 @@ public function processAjaxPost(array $translateParams) $storeId = $validStoreId; } } - $resource->saveTranslate($param['original'], $param['custom'], null, $storeId); + $resource->saveTranslate( + $param['original'], + $param['custom'], + null, + $storeId + ); } return $this->getCacheManger()->updateAndGetTranslations(); @@ -257,7 +262,7 @@ protected function _validateTranslationParams(array $translateParams) * Apply input filter to values of translation parameters * * @param array $translateParams - * @param array $fieldNames + * @param array $fieldNames Names of fields values of which are to be filtered * @return void */ protected function _filterTranslationParams(array &$translateParams, array $fieldNames) diff --git a/app/code/Magento/Translation/Model/Js/DataProvider.php b/app/code/Magento/Translation/Model/Js/DataProvider.php index 7aad7c765bcd5..ae388239bc538 100644 --- a/app/code/Magento/Translation/Model/Js/DataProvider.php +++ b/app/code/Magento/Translation/Model/Js/DataProvider.php @@ -120,6 +120,8 @@ public function getData($themePath) } } + ksort($dictionary); + return $dictionary; } diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml new file mode 100644 index 0000000000000..155e174310ea9 --- /dev/null +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml @@ -0,0 +1,68 @@ +<?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="StorefrontButtonsInlineTranslationTest"> + <annotations> + <features value="Translation"/> + <stories value="Inline Translation"/> + <title value="[Inline Translation] Buttons inline translation"/> + <description value="[Inline Translation] Buttons inline translation"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12735"/> + <group value="translation"/> + <skip> + <issueId value="MC-20127"/> + </skip> + </annotations> + <before> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Enable Translate Inline For Storefront--> + <magentoCLI + command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" + stepKey="enableTranslateInlineForStorefront"/> + <!-- Set developer mode --> + <magentoCLI command="deploy:mode:set developer" stepKey="setDeveloperMode"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Disable Translate Inline For Storefront --> + <magentoCLI + command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" + stepKey="disableTranslateInlineForStorefront"/> + <!-- Set production mode --> + <magentoCLI command="deploy:mode:set production" stepKey="setProductionMode"/> + + <!-- Delete Simple Product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add product to cart on storefront --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Click on cart button on the top --> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="showMiniCart"/> + + <!-- Small cart popup appeared. --> + <waitForElementVisible selector="{{StorefrontMinicartSection.productName}}" stepKey="seeProductNameAppeared"/> + + <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> + <actionGroup ref="AssertElementInTranslateInlineModeActionGroup" stepKey="assertRedBordersAndBookIcon"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + </actionGroup> + + <actionGroup ref="AdminTranslateElementActionGroup" stepKey="translateProceedToCheckoutButtonText"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + <argument name="translateText" value="Proceed to Checkout Translated"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php b/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php index 021709bdda1f6..b5bfbbc29a603 100644 --- a/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php +++ b/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php @@ -90,7 +90,7 @@ public function testGetData() $themePath = 'blank'; $areaCode = 'adminhtml'; - $filePaths = [['path1'], ['path2'], ['path3'], ['path4']]; + $filePaths = [['path1'], ['path2'], ['path4'], ['path3']]; $jsFilesMap = [ ['base', $themePath, '*', '*', [$filePaths[0]]], @@ -111,8 +111,8 @@ public function testGetData() $contentsMap = [ 'content1$.mage.__("hello1")content1', 'content2$.mage.__("hello2")content2', + 'content2$.mage.__("hello4")content4', // this value should be last after running data provider 'content2$.mage.__("hello3")content3', - 'content2$.mage.__("hello4")content4' ]; $translateMap = [ @@ -147,7 +147,13 @@ public function testGetData() ->method('render') ->willReturnMap($translateMap); - $this->assertEquals($expectedResult, $this->model->getData($themePath)); + $actualResult = $this->model->getData($themePath); + $this->assertEquals($expectedResult, $actualResult); + $this->assertEquals( + json_encode($expectedResult), + json_encode($actualResult), + "Translations should be sorted by key" + ); } /** diff --git a/app/code/Magento/Translation/composer.json b/app/code/Magento/Translation/composer.json index c01791c88f99f..e88f44e7cd039 100644 --- a/app/code/Magento/Translation/composer.json +++ b/app/code/Magento/Translation/composer.json @@ -9,7 +9,8 @@ "magento/framework": "*", "magento/module-backend": "*", "magento/module-developer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-theme": "*" }, "suggest": { "magento/module-deploy": "*" diff --git a/app/code/Magento/Translation/etc/db_schema.xml b/app/code/Magento/Translation/etc/db_schema.xml index a0d08467acf06..a8ce30a0b4fd9 100644 --- a/app/code/Magento/Translation/etc/db_schema.xml +++ b/app/code/Magento/Translation/etc/db_schema.xml @@ -9,11 +9,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="translation" resource="default" engine="innodb" comment="Translations"> <column xsi:type="int" name="key_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Key Id of Translation"/> + comment="Key ID of Translation"/> <column xsi:type="varchar" name="string" nullable="false" length="255" default="Translate String" comment="Translation String"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="translate" nullable="true" length="255" comment="Translate"/> <column xsi:type="varchar" name="locale" nullable="false" length="20" default="en_US" comment="Locale"/> <column xsi:type="bigint" name="crc_string" padding="20" unsigned="false" nullable="false" identity="false" diff --git a/app/code/Magento/Translation/view/base/templates/translate.phtml b/app/code/Magento/Translation/view/base/templates/translate.phtml index 445c3e88830a6..4c257eb76843f 100644 --- a/app/code/Magento/Translation/view/base/templates/translate.phtml +++ b/app/code/Magento/Translation/view/base/templates/translate.phtml @@ -6,6 +6,10 @@ /** @var \Magento\Translation\Block\Js $block */ ?> +<!-- + For frontend area dictionary file is inserted into html head in Magento/Translation/view/base/templates/dictionary.phtml + Same translation mechanism should be introduced for admin area in 2.4 version. +--> <?php if ($block->dictionaryEnabled()) : ?> <script> require.config({ diff --git a/app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js b/app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js new file mode 100644 index 0000000000000..72f497dde9ad8 --- /dev/null +++ b/app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'text!js-translation.json' +], function (dict) { + 'use strict'; + + return JSON.parse(dict); +}); diff --git a/app/code/Magento/Translation/view/frontend/layout/default.xml b/app/code/Magento/Translation/view/frontend/layout/default.xml new file mode 100644 index 0000000000000..244c0464301de --- /dev/null +++ b/app/code/Magento/Translation/view/frontend/layout/default.xml @@ -0,0 +1,14 @@ +<?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> + <referenceBlock name="head.additional"> + <block class="Magento\Translation\Block\Html\Head\Config" name="translate-config"/> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/Translation/view/frontend/requirejs-config.js b/app/code/Magento/Translation/view/frontend/requirejs-config.js index b4b3ce0f8c554..b5351b9d471cf 100644 --- a/app/code/Magento/Translation/view/frontend/requirejs-config.js +++ b/app/code/Magento/Translation/view/frontend/requirejs-config.js @@ -8,10 +8,12 @@ var config = { '*': { editTrigger: 'mage/edit-trigger', addClass: 'Magento_Translation/js/add-class', - 'Magento_Translation/add-class': 'Magento_Translation/js/add-class' + 'Magento_Translation/add-class': 'Magento_Translation/js/add-class', + mageTranslationDictionary: 'Magento_Translation/js/mage-translation-dictionary' } }, deps: [ - 'mage/translate-inline' + 'mage/translate-inline', + 'mageTranslationDictionary' ] }; diff --git a/app/code/Magento/Ui/Component/Control/SplitButton.php b/app/code/Magento/Ui/Component/Control/SplitButton.php index ef57268566ba8..5c9d09565fc66 100644 --- a/app/code/Magento/Ui/Component/Control/SplitButton.php +++ b/app/code/Magento/Ui/Component/Control/SplitButton.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Component\Control; /** @@ -22,7 +23,7 @@ class SplitButton extends Button { /** - * {@inheritdoc} + * @inheritdoc */ protected function getTemplatePath() { @@ -83,12 +84,12 @@ public function getButtonAttributesHtml() 'style' => $this->getStyle(), ]; - if (($idHard = $this->getIdHard())) { + if ($idHard = $this->getIdHard()) { $attributes['id'] = $idHard; } //TODO perhaps we need to skip data-mage-init when disabled="disabled" - if (($dataAttribute = $this->getDataAttribute())) { + if ($dataAttribute = $this->getDataAttribute()) { $this->getDataAttributes($dataAttribute, $attributes); } @@ -112,7 +113,7 @@ public function getToggleAttributesHtml() $title = $this->getLabel(); } - if (($currentClass = $this->getClass())) { + if ($currentClass = $this->getClass()) { $classes[] = $currentClass; } @@ -201,12 +202,11 @@ public function hasSplit() { return $this->hasData('has_split') ? (bool)$this->getData('has_split') : true; } - /** * Add data attributes to $attributes array * * @param array $data - * @param array &$attributes + * @param array $attributes * @return void */ protected function getDataAttributes($data, &$attributes) diff --git a/app/code/Magento/Ui/Component/Form/Element/AbstractOptionsField.php b/app/code/Magento/Ui/Component/Form/Element/AbstractOptionsField.php index b01b2937151ce..586d76828ba3a 100644 --- a/app/code/Magento/Ui/Component/Form/Element/AbstractOptionsField.php +++ b/app/code/Magento/Ui/Component/Form/Element/AbstractOptionsField.php @@ -62,6 +62,14 @@ public function prepare() if (empty($config['rawOptions'])) { $options = $this->convertOptionsValueToString($options); } + + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); + $config['options'] = array_values(array_replace_recursive($config['options'], $options)); } $this->setData('config', (array)$config); @@ -89,7 +97,7 @@ protected function convertOptionsValueToString(array $options) { array_walk( $options, - static function (&$value) { + function (&$value) { if (isset($value['value']) && is_scalar($value['value'])) { $value['value'] = (string)$value['value']; } diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php index 470767af6d319..31d2fe786cfd8 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php @@ -111,14 +111,7 @@ public function getComponentName() public function convertDate($date, $hour = 0, $minute = 0, $second = 0, $setUtcTimeZone = true) { try { - $dateObj = $this->localeDate->date( - new \DateTime( - $date, - new \DateTimeZone($this->localeDate->getConfigTimezone()) - ), - $this->getLocale(), - true - ); + $dateObj = $this->localeDate->date($date, $this->getLocale(), true); $dateObj->setTime($hour, $minute, $second); //convert store date to default date in UTC timezone without DST if ($setUtcTimeZone) { diff --git a/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php b/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php index d39d2dc3cd930..040c27d4939ef 100644 --- a/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php +++ b/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Component\Form\Element; use Magento\Framework\Data\Form\Element\Editor; use Magento\Framework\Data\Form; use Magento\Framework\Data\FormFactory; -use Magento\Framework\DataObject; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Wysiwyg\ConfigInterface; @@ -51,6 +51,7 @@ public function __construct( array $config = [] ) { $wysiwygConfigData = isset($config['wysiwygConfigData']) ? $config['wysiwygConfigData'] : []; + $this->form = $formFactory->create(); $wysiwygId = $context->getNamespace() . '_' . $data['name']; $this->editor = $this->form->addField( diff --git a/app/code/Magento/Ui/Component/MassAction.php b/app/code/Magento/Ui/Component/MassAction.php index 4cca8d4c012bb..5af263dd861ce 100644 --- a/app/code/Magento/Ui/Component/MassAction.php +++ b/app/code/Magento/Ui/Component/MassAction.php @@ -28,7 +28,7 @@ public function prepare() if ($disabledAction) { continue; } - $config['actions'][] = $componentConfig; + $config['actions'][] = array_merge($componentConfig, ['__disableTmpl' => true]); } $origConfig = $this->getConfiguration(); diff --git a/app/code/Magento/Ui/Model/BookmarkSearchResults.php b/app/code/Magento/Ui/Model/BookmarkSearchResults.php new file mode 100644 index 0000000000000..2171a5c0084e2 --- /dev/null +++ b/app/code/Magento/Ui/Model/BookmarkSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ui\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Ui\Api\Data\BookmarkSearchResultsInterface; + +/** + * Service Data Object with Bookmark search results. + */ +class BookmarkSearchResults extends SearchResults implements BookmarkSearchResultsInterface +{ +} diff --git a/app/code/Magento/Ui/Model/Export/MetadataProvider.php b/app/code/Magento/Ui/Model/Export/MetadataProvider.php old mode 100644 new mode 100755 index 603e102fa30e0..3f6685e37fcc4 --- a/app/code/Magento/Ui/Model/Export/MetadataProvider.php +++ b/app/code/Magento/Ui/Model/Export/MetadataProvider.php @@ -15,6 +15,7 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** + * Metadata Provider * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MetadataProvider @@ -84,7 +85,7 @@ protected function getColumnsComponent(UiComponentInterface $component) return $childComponent; } } - throw new \Exception('No columns found'); + throw new \Exception('No columns found'); // @codingStandardsIgnoreLine } /** @@ -119,12 +120,6 @@ public function getHeaders(UiComponentInterface $component) $row[] = $column->getData('config/label'); } - array_walk($row, function (&$header) { - if (mb_strpos($header, 'ID') === 0) { - $header = '"' . $header . '"'; - } - }); - return $row; } diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml deleted file mode 100644 index 8dc20142add3f..0000000000000 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?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="successMessage" type="text" selector=".message-success"/> - <element name="errorMessage" type="text" selector=".message.message-error.error"/> - <element name="warningMessage" type="text" selector=".message-warning"/> - <element name="noticeMessage" type="text" selector=".message-notice"/> - <element name="accessDenied" type="text" selector=".access-denied-page"/> - </section> -</sections> diff --git a/app/code/Magento/Ui/Test/Unit/Component/MassActionTest.php b/app/code/Magento/Ui/Test/Unit/Component/MassActionTest.php index b2f494351597f..c2e064bb3b069 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/MassActionTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/MassActionTest.php @@ -104,7 +104,8 @@ public function getPrepareDataProvider() [ 'type' => 'first_action', 'label' => 'First Action', - 'url' => '/module/controller/firstAction' + 'url' => '/module/controller/firstAction', + '__disableTmpl' => true ], ], [ @@ -123,7 +124,8 @@ public function getPrepareDataProvider() 'label' => 'Second Sub Action 2', 'url' => '/module/controller/secondSubAction2' ], - ] + ], + '__disableTmpl' => true ], ], ]; diff --git a/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php b/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php old mode 100644 new mode 100755 index 80cf7666eeedd..50bce70e08feb --- a/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php +++ b/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php @@ -97,12 +97,12 @@ public function testGetHeaders(array $columnLabels, array $expected): void public function getColumnsDataProvider(): array { return [ - [['ID'],['"ID"']], + [['ID'],['ID']], [['Name'],['Name']], [['Id'],['Id']], [['id'],['id']], - [['IDTEST'],['"IDTEST"']], - [['ID TEST'],['"ID TEST"']], + [['IDTEST'],['IDTEST']], + [['ID TEST'],['ID TEST']], ]; } diff --git a/app/code/Magento/Ui/etc/db_schema.xml b/app/code/Magento/Ui/etc/db_schema.xml index 13a384024f18a..552bd267e707a 100644 --- a/app/code/Magento/Ui/etc/db_schema.xml +++ b/app/code/Magento/Ui/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="bookmark_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Bookmark identifier"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="User Id"/> + comment="User ID"/> <column xsi:type="varchar" name="namespace" nullable="false" length="255" comment="Bookmark namespace"/> <column xsi:type="varchar" name="identifier" nullable="false" length="255" comment="Bookmark Identifier"/> <column xsi:type="smallint" name="current" padding="6" unsigned="false" nullable="false" identity="false" diff --git a/app/code/Magento/Ui/etc/di.xml b/app/code/Magento/Ui/etc/di.xml index c029e18addf73..05ace9d556fa0 100644 --- a/app/code/Magento/Ui/etc/di.xml +++ b/app/code/Magento/Ui/etc/di.xml @@ -14,7 +14,7 @@ <preference for="Magento\Framework\View\Element\UiComponent\ContextInterface" type="Magento\Framework\View\Element\UiComponent\Context" /> <preference for="Magento\Framework\View\Element\UiComponent\LayoutInterface" type="Magento\Framework\View\Layout\Generic"/> <preference for="Magento\Authorization\Model\UserContextInterface" type="Magento\Authorization\Model\CompositeUserContext"/> - <preference for="Magento\Ui\Api\Data\BookmarkSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Ui\Api\Data\BookmarkSearchResultsInterface" type="Magento\Ui\Model\BookmarkSearchResults" /> <preference for="Magento\Ui\Api\BookmarkRepositoryInterface" type="Magento\Ui\Model\ResourceModel\BookmarkRepository"/> <preference for="Magento\Ui\Api\Data\BookmarkInterface" type="Magento\Ui\Model\Bookmark"/> <preference for="Magento\Ui\Component\Wysiwyg\ConfigInterface" type="Magento\Ui\Component\Wysiwyg\Config"/> diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js b/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js index b33f0b5c72395..53580fc069c47 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js @@ -155,7 +155,7 @@ define([ updateExternalValueByEditableData: function () { var updatedExtValue; - if (!this.behaviourType === 'edit' || _.isEmpty(this.editableData) || _.isEmpty(this.externalValue())) { + if (!(this.behaviourType === 'edit') || _.isEmpty(this.editableData) || _.isEmpty(this.externalValue())) { return; } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index f28569caa0053..73bef62910644 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -435,9 +435,10 @@ define([ * @param {String} constructedMessage */ insertMethod: function (constructedMessage) { - var errorMsgBodyHtml = '<strong>%s</strong> %s.<br>' - .replace('%s', error.filename) - .replace('%s', $t('was not uploaded')); + var escapedFileName = $('<div>').text(error.filename).html(), + errorMsgBodyHtml = '<strong>%s</strong> %s.<br>' + .replace('%s', escapedFileName) + .replace('%s', $t('was not uploaded')); // html is escaped in message body for notification widget; prepend unescaped html here constructedMessage = constructedMessage.replace('%s', errorMsgBodyHtml); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js index 0eaacdc32567b..72c352f353239 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js @@ -8,14 +8,14 @@ */ define([ 'underscore', - 'uiRegistry', './abstract' -], function (_, registry, Abstract) { +], function (_, Abstract) { 'use strict'; return Abstract.extend({ defaults: { imports: { + countryOptions: '${ $.parentName }.country_id:indexedOptions', update: '${ $.parentName }.country_id:value' } }, @@ -41,31 +41,32 @@ define([ }, /** - * @param {String} value + * Method called every time country selector's value gets changed. + * Updates all validations and requirements for certain country. + * @param {String} value - Selected country ID. */ update: function (value) { - var country = registry.get(this.parentName + '.' + 'country_id'), - options = country.indexedOptions, - option = null; + var isZipCodeOptional, + option; if (!value) { return; } - option = options[value]; + option = _.isObject(this.countryOptions) && this.countryOptions[value]; if (!option) { return; } - if (option['is_zipcode_optional']) { + isZipCodeOptional = !!option['is_zipcode_optional']; + + if (isZipCodeOptional) { this.error(false); - this.validation = _.omit(this.validation, 'required-entry'); - } else { - this.validation['required-entry'] = true; } - this.required(!option['is_zipcode_optional']); + this.validation['required-entry'] = !isZipCodeOptional; + this.required(!isZipCodeOptional); } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index f6eafcf49284d..cd9c2aee85dc6 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -18,81 +18,54 @@ define([ defaults: { skipValidation: false, imports: { + countryOptions: '${ $.parentName }.country_id:indexedOptions', update: '${ $.parentName }.country_id:value' } }, /** - * @param {String} value + * Method called every time country selector's value gets changed. + * Updates all validations and requirements for certain country. + * @param {String} value - Selected country ID. */ update: function (value) { - var country = registry.get(this.parentName + '.' + 'country_id'), - options = country.indexedOptions, - isRegionRequired, + var isRegionRequired, option; if (!value) { return; } - option = options[value]; - if (typeof option === 'undefined') { + option = _.isObject(this.countryOptions) && this.countryOptions[value]; + + if (!option) { return; } defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); - if (this.skipValidation) { - this.validation['required-entry'] = false; - this.required(false); - } else { - if (option && !option['is_region_required']) { - this.error(false); - this.validation = _.omit(this.validation, 'required-entry'); - registry.get(this.customName, function (input) { - input.validation['required-entry'] = false; - input.required(false); - }); - } else { - this.validation['required-entry'] = true; - } + if (option['is_region_visible'] === false) { + // Hide select and corresponding text input field if region must not be shown for selected country. + this.setVisible(false); - if (option && !this.options().length) { - registry.get(this.customName, function (input) { - isRegionRequired = !!option['is_region_required']; - input.validation['required-entry'] = isRegionRequired; - input.validation['validate-not-number-first'] = true; - input.required(isRegionRequired); - }); + if (this.customEntry) { // eslint-disable-line max-depth + this.toggleInput(false); } - - this.required(!!option['is_region_required']); } - }, - /** - * Filters 'initialOptions' property by 'field' and 'value' passed, - * calls 'setOptions' passing the result to it - * - * @param {*} value - * @param {String} field - */ - filter: function (value, field) { - var superFn = this._super; - - registry.get(this.parentName + '.' + 'country_id', function (country) { - var option = country.indexedOptions[value]; + isRegionRequired = !this.skipValidation && !!option['is_region_required']; - superFn.call(this, value, field); + if (!isRegionRequired) { + this.error(false); + } - if (option && option['is_region_visible'] === false) { - // hide select and corresponding text input field if region must not be shown for selected country - this.setVisible(false); + this.required(isRegionRequired); + this.validation['required-entry'] = isRegionRequired; - if (this.customEntry) {// eslint-disable-line max-depth - this.toggleInput(false); - } - } + registry.get(this.customName, function (input) { + input.required(isRegionRequired); + input.validation['required-entry'] = isRegionRequired; + input.validation['validate-not-number-first'] = !this.options().length; }.bind(this)); } }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js new file mode 100644 index 0000000000000..2549fa93a834f --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -0,0 +1,251 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/grid/columns/column', + 'Magento_Ui/js/lib/key-codes' +], function ($, Column, keyCodes) { + 'use strict'; + + return Column.extend({ + defaults: { + bodyTmpl: 'ui/grid/columns/image-preview', + previewImageSelector: '[data-image-preview]', + visibleRecord: null, + height: 0, + displayedRecord: {}, + lastOpenedImage: null, + fields: { + previewUrl: 'preview_url', + title: 'title' + }, + modules: { + masonry: '${ $.parentName }', + thumbnailComponent: '${ $.parentName }.thumbnail_url' + }, + statefull: { + sorting: true, + lastOpenedImage: true + }, + listens: { + '${ $.provider }:params.filters': 'hide', + '${ $.provider }:params.search': 'hide', + '${ $.provider }:params.paging': 'hide' + }, + exports: { + height: '${ $.parentName }.thumbnail_url:previewHeight' + } + }, + + /** + * Initialize image preview component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + $(document).on('keydown', this.handleKeyDown.bind(this)); + + return this; + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'visibleRecord', + 'height', + 'displayedRecord', + 'lastOpenedImage' + ]); + + return this; + }, + + /** + * Next image preview + * + * @param {Object} record + */ + next: function (record) { + var recordToShow; + + if (record._rowIndex + 1 === this.masonry().rows().length) { + return; + } + + recordToShow = this.getRecord(record._rowIndex + 1); + recordToShow.rowNumber = record.lastInRow ? record.rowNumber + 1 : record.rowNumber; + this.show(recordToShow); + }, + + /** + * Previous image preview + * + * @param {Object} record + */ + prev: function (record) { + var recordToShow; + + if (record._rowIndex === 0) { + return; + } + recordToShow = this.getRecord(record._rowIndex - 1); + + recordToShow.rowNumber = record.firstInRow ? record.rowNumber - 1 : record.rowNumber; + this.show(recordToShow); + }, + + /** + * Get record + * + * @param {Integer} recordIndex + * + * @return {Object} + */ + getRecord: function (recordIndex) { + return this.masonry().rows()[recordIndex]; + }, + + /** + * Set selected row id + * + * @param {Number} rowId + * @private + */ + _selectRow: function (rowId) { + this.thumbnailComponent().previewRowId(rowId); + }, + + /** + * Show image preview + * + * @param {Object} record + */ + show: function (record) { + var img; + + if (record._rowIndex === this.visibleRecord()) { + this.hide(); + + return; + } + + this.hide(); + this.displayedRecord(record); + this._selectRow(record.rowNumber || null); + this.visibleRecord(record._rowIndex); + + img = $(this.previewImageSelector + ' img'); + + if (img.get(0).complete) { + this.updateHeight(); + this.scrollToPreview(); + } else { + img.load(function () { + this.updateHeight(); + this.scrollToPreview(); + }.bind(this)); + } + + this.lastOpenedImage(record._rowIndex); + }, + + /** + * Update image preview section height + */ + updateHeight: function () { + this.height($(this.previewImageSelector).height() + 'px'); + }, + + /** + * Close image preview + */ + hide: function () { + this.lastOpenedImage(null); + this.visibleRecord(null); + this.height(0); + this._selectRow(null); + }, + + /** + * Returns visibility for given record. + * + * @param {Object} record + * @return {*|bool} + */ + isVisible: function (record) { + if (this.lastOpenedImage() === record._rowIndex && + this.visibleRecord() === null + ) { + this.show(record); + } + + return this.visibleRecord() === record._rowIndex || false; + }, + + /** + * Returns preview image url for a given record. + * + * @param {Object} record + * @return {String} + */ + getUrl: function (record) { + return record[this.fields.previewUrl]; + }, + + /** + * Returns image title for a given record. + * + * @param {Object} record + * @return {String} + */ + getTitle: function (record) { + return record[this.fields.title]; + }, + + /** + * Get styles for preview + * + * @returns {Object} + */ + getStyles: function () { + return { + 'margin-top': '-' + this.height() + }; + }, + + /** + * Scroll to preview window + */ + scrollToPreview: function () { + $(this.previewImageSelector).get(0).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); + }, + + /** + * Handle keyboard navigation for image preview + * + * @param {Object} e + */ + handleKeyDown: function (e) { + var key = keyCodes[e.keyCode]; + + if (this.visibleRecord() !== null) { + if (key === 'pageLeftKey') { + this.prev(this.displayedRecord()); + } else if (key === 'pageRightKey') { + this.next(this.displayedRecord()); + } + } + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js new file mode 100644 index 0000000000000..cec0955b835e0 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js @@ -0,0 +1,90 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/columns/column' +], function (Column) { + 'use strict'; + + return Column.extend({ + defaults: { + bodyTmpl: 'ui/grid/columns/image', + modules: { + previewComponent: '${ $.parentName }.preview' + }, + previewRowId: null, + previewHeight: 0, + fields: { + id: 'id', + url: 'url' + } + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'previewRowId', + 'previewHeight' + ]); + + return this; + }, + + /** + * Returns url to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getUrl: function (record) { + return record[this.fields.url]; + }, + + /** + * Returns id to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Number} + */ + getId: function (record) { + return record[this.fields.id]; + }, + + /** + * Returns container styles to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Object} + */ + getStyles: function (record) { + var styles = record.styles(); + + styles['margin-bottom'] = this.previewRowId() === record.rowNumber ? this.previewHeight : 0; + record.styles(styles); + + return record.styles; + }, + + /** + * Returns class list to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Object} + */ + getClasses: function (record) { + return record.css || {}; + }, + + /** + * Expand image preview + */ + expandPreview: function (record) { + this.previewComponent().show(record); + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/overlay.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/overlay.js new file mode 100644 index 0000000000000..420b318e0b440 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/overlay.js @@ -0,0 +1,35 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/columns/column' +], function (Column) { + 'use strict'; + + return Column.extend({ + defaults: { + bodyTmpl: 'ui/grid/columns/overlay' + }, + + /** + * If overlay should be visible + * + * @param {Object} row + * @returns {Boolean} + */ + isVisible: function (row) { + return !!row[this.index]; + }, + + /** + * Get overlay label + * + * @param {Object} row + * @returns {String} + */ + getLabel: function (row) { + return row[this.index]; + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js index ece49cc8fe27c..ad70b200e4420 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js @@ -337,7 +337,18 @@ define([ * @returns {Object} Collection of records data. */ getData: function () { - var data = this.activeRecords.map('getData'); + var data = this.activeRecords.map(function (record) { + var elemKey, + recordData = record.getData(); + + for (elemKey in recordData) { + if (_.isUndefined(recordData[elemKey])) { + recordData[elemKey] = null; + } + } + + return recordData; + }); return _.indexBy(data, this.indexField); }, diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js index 98c3eb1c6f882..78016ee489a11 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js @@ -272,11 +272,22 @@ define([ } filter = utils.extend({}, filters.base, filter); + //Accepting labels as is. + filter.__disableTmpl = { + label: 1, + options: 1 + }; - return utils.template(filter, { + filter = utils.template(filter, { filters: this, column: column }, true, true); + + filter.__disableTmpl = { + label: true + }; + + return filter; }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/masonry.js b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js new file mode 100644 index 0000000000000..e4c72ee950c26 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js @@ -0,0 +1,276 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/listing', + 'Magento_Ui/js/lib/view/utils/raf', + 'jquery', + 'ko', + 'underscore' +], function (Listing, raf, $, ko, _) { + 'use strict'; + + return Listing.extend({ + defaults: { + template: 'ui/grid/masonry', + imports: { + rows: '${ $.provider }:data.items', + errorMessage: '${ $.provider }:data.errorMessage' + }, + listens: { + rows: 'initComponent' + }, + + /** + * Images container id + * @param string + */ + containerId: null, + + /** + * Minimum aspect ratio for each image + * @param int + */ + minRatio: null, + + /** + * Container width + * @param int + */ + containerWidth: window.innerWidth, + + /** + * Margin between images + * @param int + */ + imageMargin: 20, + + /** + * Maximum image height value + * @param int + */ + maxImageHeight: 240, + + /** + * The value is minimum image width to height ratio when container width is less than the key + * @param {Object} + */ + containerWidthToMinRatio: { + 640: 3, + 1280: 5, + 1920: 8 + }, + + /** + * Default minimal image width to height ratio. + * Applied when container width is greater than max width in the containerWidthToMinRatio matrix. + * @param int + */ + defaultMinRatio: 10, + + /** + * Layout update FPS during window resizing + */ + refreshFPS: 60 + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'rows', + 'errorMessage' + ]); + + return this; + }, + + /** + * Init component handler + * @param {Object} rows + * @return {Object} + */ + initComponent: function (rows) { + if (!rows.length) { + return; + } + this.imageMargin = parseInt(this.imageMargin, 10); + this.container = $('[data-id="' + this.containerId + '"]')[0]; + + this.setLayoutStyles(); + this.setEventListener(); + + return this; + }, + + /** + * Set event listener to track resize event + */ + setEventListener: function () { + window.addEventListener('resize', function () { + raf(function () { + this.containerWidth = window.innerWidth; + this.setLayoutStyles(); + }.bind(this), this.refreshFPS); + }.bind(this)); + }, + + /** + * Set layout styles inside the container + */ + setLayoutStyles: function () { + var containerWidth = parseInt(this.container.clientWidth, 10), + rowImages = [], + ratio = 0, + rowHeight = 0, + calcHeight = 0, + isLastRow = false, + rowNumber = 1; + + this.setMinRatio(); + + this.rows().forEach(function (image, index) { + ratio += parseFloat((image.width / image.height).toFixed(2)); + rowImages.push(image); + + if (ratio < this.minRatio && index + 1 !== this.rows().length) { + // Row has more space for images and the image is not the last one - proceed to the next iteration + return; + } + + ratio = Math.max(ratio, this.minRatio); + calcHeight = (containerWidth - this.imageMargin * rowImages.length) / ratio; + rowHeight = calcHeight < this.maxImageHeight ? calcHeight : this.maxImageHeight; + isLastRow = index + 1 === this.rows().length; + + this.assignImagesToRow(rowImages, rowNumber, rowHeight, isLastRow); + + rowImages = []; + ratio = 0; + rowNumber++; + + }.bind(this)); + }, + + /** + * Apply styles, css classes and add properties for images in the row + * + * @param {Object[]} images + * @param {Number} rowNumber + * @param {Number} rowHeight + * @param {Boolean} isLastRow + */ + assignImagesToRow: function (images, rowNumber, rowHeight, isLastRow) { + var imageWidth; + + images.forEach(function (img) { + imageWidth = rowHeight * (img.width / img.height).toFixed(2); + this.setImageStyles(img, imageWidth, rowHeight); + this.setImageClass(img, { + bottom: isLastRow + }); + img.rowNumber = rowNumber; + }.bind(this)); + + images[0].firstInRow = true; + images[images.length - 1].lastInRow = true; + }, + + /** + * Wait for container to initialize + */ + waitForContainer: function (callback) { + if (typeof this.container === 'undefined') { + setTimeout(function () { + this.waitForContainer(callback); + }.bind(this), 500); + } else { + setTimeout(callback, 0); + } + }, + + /** + * Set layout styles when container element is loaded. + */ + setLayoutStylesWhenLoaded: function () { + this.waitForContainer(function () { + this.setLayoutStyles(); + }.bind(this)); + }, + + /** + * Set styles for every image in layout + * + * @param {Object} img + * @param {Number} width + * @param {Number} height + */ + setImageStyles: function (img, width, height) { + if (!img.styles) { + img.styles = ko.observable(); + } + img.styles({ + width: parseInt(width, 10) + 'px', + height: parseInt(height, 10) + 'px' + }); + }, + + /** + * Set css classes to and an image + * + * @param {Object} image + * @param {Object} classes + */ + setImageClass: function (image, classes) { + if (!image.css) { + image.css = ko.observable(classes); + } + image.css(classes); + }, + + /** + * Set min ratio for images in layout + */ + setMinRatio: function () { + var minRatio = _.find( + this.containerWidthToMinRatio, + + /** + * Find the minimal ratio for container width in the matrix + * + * @param {Number} ratio + * @param {Number} width + * @returns {Boolean} + */ + function (ratio, width) { + return this.containerWidth <= width; + }, + this + ); + + this.minRatio = minRatio ? minRatio : this.defaultMinRatio; + }, + + /** + * Checks if grid has data. + * + * @returns {Boolean} + */ + hasData: function () { + return !!this.rows() && !!this.rows().length; + }, + + /** + * Returns error message returned by the data provider + * + * @returns {String|null} + */ + getErrorMessageUnsanitizedHtml: function () { + return this.errorMessage(); + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js index 2925c5859eddd..6ff7c1f673213 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js @@ -222,10 +222,14 @@ define([ ko.utils.setTextContent(option, allBindings.get('optionsCaption')); ko.selectExtensions.writeValue(option, undefined); } else if (typeof arrayEntry[optionsValue] === 'undefined') { // empty value === optgroup - option = utils.template(optgroupTmpl, { - label: arrayEntry[optionsText], - title: arrayEntry[optionsText + 'title'] - }); + if (arrayEntry.__disableTmpl) { + option = '<optgroup label="' + arrayEntry[optionsText] + '"></optgroup>'; + } else { + option = utils.template(optgroupTmpl, { + label: arrayEntry[optionsText], + title: arrayEntry[optionsText + 'title'] + }); + } option = ko.utils.parseHtmlFragment(option)[0]; } else { @@ -307,6 +311,10 @@ define([ obj.disabled = disabled; } + if (option.hasOwnProperty('__disableTmpl')) { + obj.__disableTmpl = option.__disableTmpl; + } + label = label.replace(nbspRe, '').trim(); if (Array.isArray(value)) { diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 3402d1d1df03b..08f67955976c4 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -1067,6 +1067,16 @@ define([ return new RegExp(param).test(value); }, $.mage.__('This link is not allowed.') + ], + 'validate-dob': [ + function (value) { + if (value === '') { + return true; + } + + return moment(value).isBefore(moment()); + }, + $.mage.__('The Date of Birth should not be greater than today.') ] }, function (data) { return { diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal.js b/app/code/Magento/Ui/view/base/web/js/modal/modal.js index a8a76206bcd2b..cefe79b42e503 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal.js @@ -131,7 +131,10 @@ define([ this._createWrapper(); this._renderModal(); this._createButtons(); - $(this.options.trigger).on('click', _.bind(this.toggleModal, this)); + + if (this.options.trigger) { + $(document).on('click', this.options.trigger, _.bind(this.toggleModal, this)); + } this._on(this.modal.find(this.options.modalCloseBtn), { 'click': this.options.modalCloseBtnHandler ? this.options.modalCloseBtnHandler : this.closeModal }); diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/image-preview.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image-preview.html new file mode 100644 index 0000000000000..3b430cf2dcbdc --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image-preview.html @@ -0,0 +1,22 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="masonry-image-preview" if="$col.isVisible($row())" data-image-preview ko-style="$col.getStyles($row())"> + <div class="container"> + <div class="action-buttons"> + <button class="action-previous" type="button" click="$col.prev.bind($col, $row())"> + <span translate="'Previous'"/> + </button> + <button class="action-next" type="button" click="$col.next.bind($col, $row())"> + <span translate="'Next'"/> + </button> + <button class="action-close" type="button" click="$col.hide.bind($col)"> + <span translate="'Close'"/> + </button> + </div> + <img class="preview" attr="src: $col.getUrl($row()), alt: $col.getTitle($row())"> + </div> +</div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html new file mode 100644 index 0000000000000..c513ddeff9895 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="masonry-image-block" ko-style="$col.getStyles($row())" attr="'data-id': $col.getId($row())"> + <img attr="src: $col.getUrl($row())" css="$col.getClasses($row())" click="function(){ expandPreview($row()) }" data-role="thumbnail"/> +</div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/overlay.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/overlay.html new file mode 100644 index 0000000000000..3cdc78c0683cb --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/grid/columns/overlay.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div if="$col.isVisible($row())" class="masonry-image-overlay"> + <span text="$col.getLabel($row())"/> +</div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/masonry.html b/app/code/Magento/Ui/view/base/web/templates/grid/masonry.html new file mode 100644 index 0000000000000..089ee21bec15c --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/grid/masonry.html @@ -0,0 +1,17 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div data-role="grid-wrapper" class="masonry-image-grid" attr="'data-id': containerId"> + <div class="masonry-image-column" repeat="foreach: rows, item: '$row'"> + <div outerfasteach="data: getVisible(), as: '$col'" template="getBody()"/> + </div> + <div if="!hasData() && !getErrorMessageUnsanitizedHtml()" class="no-data-message-container"> + <span translate="'We couldn\'t find any records.'"/> + </div> + <div if="getErrorMessageUnsanitizedHtml()" class="error-message-container"> + <span html="getErrorMessageUnsanitizedHtml()"/> + </div> +</div> diff --git a/app/code/Magento/Ui/view/frontend/web/js/view/messages.js b/app/code/Magento/Ui/view/frontend/web/js/view/messages.js index 57a590c87179a..b34eea5aa226d 100644 --- a/app/code/Magento/Ui/view/frontend/web/js/view/messages.js +++ b/app/code/Magento/Ui/view/frontend/web/js/view/messages.js @@ -10,7 +10,8 @@ define([ 'ko', 'jquery', 'uiComponent', - '../model/messageList' + '../model/messageList', + 'jquery-ui-modules/effect-blind' ], function (ko, $, Component, globalMessages) { 'use strict'; @@ -19,6 +20,8 @@ define([ template: 'Magento_Ui/messages', selector: '[data-role=checkout-messages]', isHidden: false, + hideTimeout: 5000, + hideSpeed: 500, listens: { isHidden: 'onHiddenChange' } @@ -62,13 +65,11 @@ define([ * @param {Boolean} isHidden */ onHiddenChange: function (isHidden) { - var self = this; - // Hide message block if needed if (isHidden) { setTimeout(function () { - $(self.selector).hide('blind', {}, 500); - }, 5000); + $(this.selector).hide('blind', {}, this.hideSpeed); + }.bind(this), this.hideTimeout); } } }); diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index 5320aeb5bcc8a..9e33b86ea8215 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -9,7 +9,6 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Async\CallbackDeferred; -use Magento\Framework\Async\ProxyDeferredFactory; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\HTTP\AsyncClient\HttpResponseDeferredInterface; @@ -22,6 +21,7 @@ use Magento\Shipping\Model\Carrier\AbstractCarrierOnline; use Magento\Shipping\Model\Carrier\CarrierInterface; use Magento\Shipping\Model\Rate\Result; +use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory; use Magento\Shipping\Model\Simplexml\Element; use Magento\Ups\Helper\Config; use Magento\Shipping\Model\Shipment\Request as Shipment; @@ -239,15 +239,16 @@ public function collectRates(RateRequest $request) //To use the correct result in the callback. $this->_result = $result = $this->_getQuotes(); - return $this->deferredProxyFactory->createFor( - Result::class, - new CallbackDeferred( - function () use ($request, $result) { - $this->_result = $result; - $this->_updateFreeMethodQuote($request); - return $this->getResult(); - } - ) + return $this->deferredProxyFactory->create( + [ + 'deferred' => new CallbackDeferred( + function () use ($request, $result) { + $this->_result = $result; + $this->_updateFreeMethodQuote($request); + return $this->getResult(); + } + ) + ] ); } @@ -782,19 +783,20 @@ protected function _getXmlQuotes() new Request($url, Request::METHOD_POST, ['Content-Type' => 'application/xml'], $xmlRequest) ); - return $this->deferredProxyFactory->createFor( - Result::class, - new CallbackDeferred( - function () use ($httpResponse) { - if ($httpResponse->get()->getStatusCode() >= 400) { - $xmlResponse = ''; - } else { - $xmlResponse = $httpResponse->get()->getBody(); - } + return $this->deferredProxyFactory->create( + [ + 'deferred' => new CallbackDeferred( + function () use ($httpResponse) { + if ($httpResponse->get()->getStatusCode() >= 400) { + $xmlResponse = ''; + } else { + $xmlResponse = $httpResponse->get()->getBody(); + } - return $this->_parseXmlResponse($xmlResponse); - } - ) + return $this->_parseXmlResponse($xmlResponse); + } + ) + ] ); } @@ -1099,6 +1101,7 @@ protected function _getXmlTracking($trackings) $xmlRequest = <<<XMLAuth <?xml version="1.0" ?> <TrackRequest xml:lang="en-US"> + <IncludeMailInnovationIndicator/> <Request> <RequestAction>Track</RequestAction> <RequestOption>1</RequestOption> diff --git a/app/code/Magento/Ups/Model/Config/Backend/UpsUrl.php b/app/code/Magento/Ups/Model/Config/Backend/UpsUrl.php new file mode 100644 index 0000000000000..9db35b6a42dcc --- /dev/null +++ b/app/code/Magento/Ups/Model/Config/Backend/UpsUrl.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Ups\Model\Config\Backend; + +use Magento\Framework\App\Config\Value; +use Magento\Framework\Exception\ValidatorException; + +/** + * Represents a config URL that may point to a UPS endpoint + */ +class UpsUrl extends Value +{ + /** + * @inheritdoc + */ + public function beforeSave() + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $host = parse_url((string)$this->getValue(), \PHP_URL_HOST); + + if (!empty($host) && !preg_match('/(?:.+\.|^)ups\.com$/i', $host)) { + throw new ValidatorException(__('UPS API endpoint URL\'s must use ups.com')); + } + + return parent::beforeSave(); + } +} diff --git a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml index 4107f17dbc18c..f330695867e7c 100644 --- a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml +++ b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml @@ -12,5 +12,29 @@ <element name="carriersUpsTab" type="button" selector="#carriers_ups-head"/> <element name="carriersUpsType" type="select" selector="#carriers_ups_type"/> <element name="selectedUpsType" type="text" selector="#carriers_ups_type option[selected]"/> + <element name="carriersUPSActive" type="input" selector="#carriers_ups_active_inherit"/> + <element name="carriersUPSTypeSystem" type="input" selector="#carriers_ups_type_inherit"/> + <element name="carriersUPSAccountLive" type="input" selector="#carriers_ups_is_account_live_inherit"/> + <element name="carriersUPSGatewayXMLUrl" type="input" selector="#carriers_ups_gateway_xml_url_inherit"/> + <element name="carriersUPSModeXML" type="input" selector="#carriers_ups_mode_xml_inherit"/> + <element name="carriersUPSOriginShipment" type="input" selector="#carriers_ups_origin_shipment_inherit"/> + <element name="carriersUPSTitle" type="input" selector="#carriers_ups_title_inherit"/> + <element name="carriersUPSNegotiatedActive" type="input" selector="#carriers_ups_negotiated_active_inherit"/> + <element name="carriersUPSIncludeTaxes" type="input" selector="#carriers_ups_include_taxes_inherit"/> + <element name="carriersUPSShipmentRequestType" type="input" selector="#carriers_ups_shipment_requesttype_inherit"/> + <element name="carriersUPSContainer" type="input" selector="#carriers_ups_container_inherit"/> + <element name="carriersUPSDestType" type="input" selector="#carriers_ups_dest_type_inherit"/> + <element name="carriersUPSTrackingXmlUrl" type="input" selector="#carriers_ups_tracking_xml_url_inherit"/> + <element name="carriersUPSUnitOfMeasure" type="input" selector="#carriers_ups_unit_of_measure_inherit"/> + <element name="carriersUPSMaxPackageWeight" type="input" selector="#carriers_ups_max_package_weight_inherit"/> + <element name="carriersUPSPickup" type="input" selector="#carriers_ups_pickup_inherit"/> + <element name="carriersUPSMinPackageWeight" type="input" selector="#carriers_ups_min_package_weight_inherit"/> + <element name="carriersUPSHandlingType" type="input" selector="#carriers_ups_handling_type_inherit"/> + <element name="carriersUPSHandlingAction" type="input" selector="#carriers_ups_handling_action_inherit"/> + <element name="carriersUPSAllowedMethods" type="input" selector="#carriers_ups_allowed_methods_inherit"/> + <element name="carriersUPSFreeMethod" type="input" selector="#carriers_ups_free_method_inherit"/> + <element name="carriersUPSSpecificErrMsg" type="input" selector="#carriers_ups_specificerrmsg_inherit"/> + <element name="carriersUPSAllowSpecific" type="input" selector="#carriers_ups_sallowspecific_inherit"/> + <element name="carriersUPSSpecificCountry" type="input" selector="#carriers_ups_specificcountry"/> </section> </sections> diff --git a/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..126586669afd2 --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,66 @@ +<?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="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in UPS section--> + <comment userInput="Assert configuration are disabled in UPS section" stepKey="commentSeeDisabledUPSConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" dependentSelector="{{AdminShippingMethodsUpsSection.carriersUPSActive}}" visible="false" stepKey="expandUPSTab"/> + <waitForElementVisible selector="{{AdminShippingMethodsUpsSection.carriersUPSActive}}" stepKey="waitUPSTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSActive}}" userInput="disabled" stepKey="grabUPSActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSActiveDisabled" stepKey="assertUPSActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTypeSystem}}" userInput="disabled" stepKey="grabUPSTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSTypeDisabled" stepKey="assertUPSTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAccountLive}}" userInput="disabled" stepKey="grabUPSAccountLiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSAccountLiveDisabled" stepKey="assertUPSAccountLiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSGatewayXMLUrl}}" userInput="disabled" stepKey="grabUPSGatewayXMLUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSGatewayXMLUrlDisabled" stepKey="assertUPSGatewayXMLUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSModeXML}}" userInput="disabled" stepKey="grabUPSModeXMLDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSModeXMLDisabled" stepKey="assertUPSModeXMLDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSOriginShipment}}" userInput="disabled" stepKey="grabUPSOriginShipmentDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSOriginShipmentDisabled" stepKey="assertUPSOriginShipmentDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTitle}}" userInput="disabled" stepKey="grabUPSTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSTitleDisabled" stepKey="assertUPSTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSNegotiatedActive}}" userInput="disabled" stepKey="grabUPSNegotiatedActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSNegotiatedActiveDisabled" stepKey="assertUPSNegotiatedActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSIncludeTaxes}}" userInput="disabled" stepKey="grabUPSIncludeTaxesDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSIncludeTaxesDisabled" stepKey="assertUPSIncludeTaxesDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSShipmentRequestType}}" userInput="disabled" stepKey="grabUPSShipmentRequestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSShipmentRequestTypeDisabled" stepKey="assertUPSShipmentRequestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSContainer}}" userInput="disabled" stepKey="grabUPSContainerDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSContainerDisabled" stepKey="assertUPSContainerDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSDestType}}" userInput="disabled" stepKey="grabUPSDestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSDestTypeDisabled" stepKey="assertUPSDestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTrackingXmlUrl}}" userInput="disabled" stepKey="grabUPSTrackingXmlUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSTrackingXmlUrlDisabled" stepKey="assertUPSTrackingXmlUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSUnitOfMeasure}}" userInput="disabled" stepKey="grabUPSUnitOfMeasureDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSUnitOfMeasureDisabled" stepKey="assertUPSUnitOfMeasureDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSMaxPackageWeight}}" userInput="disabled" stepKey="grabUPSMaxPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSMaxPackageWeightDisabled" stepKey="assertUPSMaxPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSPickup}}" userInput="disabled" stepKey="grabUPSPickupDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSPickupDisabled" stepKey="assertUPSPickupDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSMinPackageWeight}}" userInput="disabled" stepKey="grabUPSMinPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSMinPackageWeightDisabled" stepKey="assertUPSMinPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSHandlingType}}" userInput="disabled" stepKey="grabUPSHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSHandlingTypeDisabled" stepKey="assertUPSHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSHandlingAction}}" userInput="disabled" stepKey="grabUPSHandlingActionDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSHandlingActionDisabled" stepKey="assertUPSHandlingActionDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAllowedMethods}}" userInput="disabled" stepKey="grabUPSAllowedMethodsDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSAllowedMethodsDisabled" stepKey="assertUPSAllowedMethodsDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSFreeMethod}}" userInput="disabled" stepKey="grabUPSFreeMethodDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSFreeMethodDisabled" stepKey="assertUPSFreeMethodDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSSpecificErrMsg}}" userInput="disabled" stepKey="grabUPSSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSSpecificErrMsgDisabled" stepKey="assertUPSSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAllowSpecific}}" userInput="disabled" stepKey="grabUPSAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSAllowSpecificDisabled" stepKey="assertUPSAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSSpecificCountry}}" userInput="disabled" stepKey="grabUPSSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSSpecificCountryDisabled" stepKey="assertUPSSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Ups/Test/Unit/Model/Config/Backend/UpsUrlTest.php b/app/code/Magento/Ups/Test/Unit/Model/Config/Backend/UpsUrlTest.php new file mode 100644 index 0000000000000..149f9378889f8 --- /dev/null +++ b/app/code/Magento/Ups/Test/Unit/Model/Config/Backend/UpsUrlTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Ups\Test\Unit\Model\Config\Backend; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Ups\Model\Config\Backend\UpsUrl; +use PHPUnit\Framework\TestCase; + +/** + * Verify behavior of UpsUrl backend type + */ +class UpsUrlTest extends TestCase +{ + + /** + * @var UpsUrl + */ + private $config; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + /** @var UpsUrl $upsUrl */ + $this->config = $objectManager->getObject(UpsUrl::class); + } + + /** + * @dataProvider validDataProvider + * @param string $data The valid data + */ + public function testBeforeSave($data = null) + { + $this->config->setValue($data); + $this->config->beforeSave(); + } + + /** + * @dataProvider invalidDataProvider + * @param string $data The invalid data + * @expectedException \Magento\Framework\Exception\ValidatorException + * @expectedExceptionMessage UPS API endpoint URL's must use ups.com + */ + public function testBeforeSaveErrors($data) + { + $this->config->setValue($data); + $this->config->beforeSave(); + } + + public function validDataProvider() + { + return [ + [], + [null], + [''], + ['http://ups.com'], + ['https://foo.ups.com'], + ['http://foo.ups.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } + + public function invalidDataProvider() + { + return [ + ['http://upsfoo.com'], + ['https://fooups.com'], + ['https://ups.com.fake.com'], + ['https://ups.info'], + ['http://ups.com.foo.com/foo/bar?baz=bash&fizz=buzz'], + ['http://fooups.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } +} diff --git a/app/code/Magento/Ups/etc/adminhtml/system.xml b/app/code/Magento/Ups/etc/adminhtml/system.xml index f1b8b22820cba..c6a2516e96170 100644 --- a/app/code/Magento/Ups/etc/adminhtml/system.xml +++ b/app/code/Magento/Ups/etc/adminhtml/system.xml @@ -56,9 +56,11 @@ </field> <field id="gateway_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Gateway URL</label> + <backend_model>Magento\Ups\Model\Config\Backend\UpsUrl</backend_model> </field> <field id="gateway_xml_url" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Gateway XML URL</label> + <backend_model>Magento\Ups\Model\Config\Backend\UpsUrl</backend_model> </field> <field id="handling_type" translate="label" type="select" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Calculate Handling Fee</label> @@ -104,6 +106,7 @@ </field> <field id="tracking_xml_url" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Tracking XML URL</label> + <backend_model>Magento\Ups\Model\Config\Backend\UpsUrl</backend_model> </field> <field id="type" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>UPS Type</label> @@ -131,7 +134,7 @@ <field id="include_taxes" translate="label" type="select" sortOrder="45" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Request Tax-Inclusive Rate</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment>When applicable, taxes (sales tax, VAT etc.) are included in the rate</comment> + <comment>When applicable, taxes (sales tax, VAT etc.) are included in the rate.</comment> </field> <field id="shipper_number" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Shipper Number</label> diff --git a/app/code/Magento/Ups/i18n/en_US.csv b/app/code/Magento/Ups/i18n/en_US.csv index baf8ecc855440..68dd34a313bd9 100644 --- a/app/code/Magento/Ups/i18n/en_US.csv +++ b/app/code/Magento/Ups/i18n/en_US.csv @@ -114,3 +114,4 @@ Title,Title Mode,Mode "This enables or disables SSL verification of the Magento server by UPS.","This enables or disables SSL verification of the Magento server by UPS." Debug,Debug +"UPS API endpoint URL's must use ups.com","UPS API endpoint URL's must use ups.com" diff --git a/app/code/Magento/UrlRewrite/Controller/Router.php b/app/code/Magento/UrlRewrite/Controller/Router.php index 47718ba36316b..dd26f49b8efa4 100644 --- a/app/code/Magento/UrlRewrite/Controller/Router.php +++ b/app/code/Magento/UrlRewrite/Controller/Router.php @@ -5,15 +5,16 @@ */ namespace Magento\UrlRewrite\Controller; +use Magento\Framework\App\Action\Redirect; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; use Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\Framework\App\Request\Http as HttpRequest; -use Magento\Framework\App\Response\Http as HttpResponse; -use Magento\Framework\UrlInterface; -use Magento\Framework\App\Action\Redirect; -use Magento\Framework\App\ActionInterface; /** * UrlRewrite Controller Router @@ -73,11 +74,12 @@ public function __construct( * * @param RequestInterface|HttpRequest $request * @return ActionInterface|null + * @throws NoSuchEntityException */ public function match(RequestInterface $request) { $rewrite = $this->getRewrite( - $this->getNormalizedPathInfo($request), + $request->getPathInfo(), $this->storeManager->getStore()->getId() ); @@ -153,30 +155,4 @@ protected function getRewrite($requestPath, $storeId) ] ); } - - /** - * Get normalized request path - * - * @param RequestInterface|HttpRequest $request - * @return string - */ - private function getNormalizedPathInfo(RequestInterface $request): string - { - $path = $request->getPathInfo(); - /** - * If request contains query params then we need to trim a slash in end of the path. - * For example: - * the original request is: http://my-host.com/category-url-key.html/?color=black - * where the original path is: category-url-key.html/ - * and the result path will be: category-url-key.html - * - * It need to except a redirect like this: - * http://my-host.com/category-url-key.html/?color=black => http://my-host.com/category-url-key.html - */ - if (!empty($path) && $request->getQuery()->count()) { - $path = rtrim($path, '/'); - } - - return (string)$path; - } } diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminProductFormUpdateUrlKeyActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminProductFormUpdateUrlKeyActionGroup.xml new file mode 100644 index 0000000000000..967aa4be7cdc8 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminProductFormUpdateUrlKeyActionGroup.xml @@ -0,0 +1,23 @@ +<?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="AdminProductFormUpdateUrlKeyActionGroup"> + <annotations> + <description>Update UrlKey for Product on Custom Store View.</description> + </annotations> + <arguments> + <argument name="newUrlKey" defaultValue="newUrlKey" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.useDefaultUrl}}" visible="false" stepKey="clickOnSearchEngineOptimization"/> + <waitForLoadingMaskToDisappear stepKey="waitLoadProductForm"/> + <uncheckOption selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="uncheckDefaultUrl"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{newUrlKey}}" stepKey="changeUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml index d3e9509f1ef00..4f89c9389c32b 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml @@ -22,20 +22,18 @@ </arguments> <amOnPage url="{{AdminUrlRewriteEditPage.url}}" stepKey="openUrlRewriteEditPage"/> - <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad"/> - <click selector="{{AdminUrlRewriteEditSection.createCustomUrlRewrite}}" stepKey="clickOnCustonUrlRewrite"/> - <click selector="{{AdminUrlRewriteEditSection.createCustomUrlRewriteValue('customUrlRewriteValue')}}" stepKey="selectForCategory"/> - <waitForPageLoad stepKey="waitForCategoryEditSectionToLoad"/> - <click selector="{{AdminUrlRewriteEditSection.categoryInTree($$category.name$$)}}" stepKey="selectCategoryInTree"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> - <click selector="{{AdminUrlRewriteEditSection.storeValue('storeValue')}}" stepKey="clickOnStoreValue"/> - <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> - <click selector="{{AdminUrlRewriteEditSection.redirectType}}" stepKey="selectRedirectType"/> - <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue('redirectTypeValue')}}" stepKey="clickOnRedirectTypeValue"/> - <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> - <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> + <waitForElementVisible selector="{{AdminUrlRewriteEditSection.createCustomUrlRewrite}}" stepKey="waitForCreateUrlRewriteVisible"/> + <selectOption selector="{{AdminUrlRewriteEditSection.createCustomUrlRewrite}}" userInput="{{customUrlRewriteValue}}" stepKey="selectUrlRewriteTypeOption"/> + <waitForElementVisible selector="{{AdminUrlRewriteEditSection.categoryInTree(category)}}" stepKey="waitForCategoryInTreeVisible"/> + <click selector="{{AdminUrlRewriteEditSection.categoryInTree(category)}}" stepKey="clickOnCategoryInTree"/> + <waitForElementVisible selector="{{AdminUrlRewriteEditSection.store}}" stepKey="waitForStoreSelectVisible"/> + <selectOption selector="{{AdminUrlRewriteEditSection.store}}" userInput="{{storeValue}}" stepKey="selectStoreOption"/> + <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPathField"/> + <selectOption selector="{{AdminUrlRewriteEditSection.redirectType}}" userInput="{{redirectTypeValue}}" stepKey="selectRedirectType"/> + <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescriptionField"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The URL Rewrite has been saved." stepKey="seeSuccessMessage"/> </actionGroup> <actionGroup name="AdminAddUrlRewriteForProduct"> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontCheckProductUrlActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontCheckProductUrlActionGroup.xml new file mode 100644 index 0000000000000..c97a6d9bb8f24 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontCheckProductUrlActionGroup.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"> + <!-- Check the simple product Url on the product page --> + <actionGroup name="StorefrontCheckProductUrlActionGroup"> + <annotations> + <description>Validates that the provided Simple Product Url is correct.</description> + </annotations> + <arguments> + <argument name="productUrl" type="string"/> + </arguments> + <seeInCurrentUrl url="{{StorefrontProductPage.url(productUrl)}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml index 44fad061d7656..121fd7c736dcc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml @@ -18,7 +18,16 @@ <group value="urlRewrite"/> </annotations> <before> + <createData entity="ApiCategory" stepKey="createCategory"> + <field key="name">category-admin</field> + </createData> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteENStoreViewIfExists"> + <argument name="storeViewName" value="{{customStoreENNotUnique.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteNLStoreViewIfExists"> + <argument name="storeViewName" value="{{customStoreNLNotUnique.name}}"/> + </actionGroup> <!-- Create Store View EN --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> <argument name="customStore" value="customStoreENNotUnique"/> @@ -27,23 +36,21 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewNl"> <argument name="customStore" value="customStoreNLNotUnique"/> </actionGroup> - <createData entity="ApiCategory" stepKey="createCategory"> - <field key="name">category-admin</field> - </createData> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteStoreViewEn"> + <argument name="storeViewName" value="{{customStoreENNotUnique.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteStoreViewNl"> + <argument name="storeViewName" value="{{customStoreNLNotUnique.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearStoreFilters"/> <actionGroup ref="deleteProductByName" stepKey="deleteImportedProduct"> <argument name="sku" value="productformagetwo68980"/> <argument name="name" value="productformagetwo68980"/> </actionGroup> <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFiltersIfSet"/> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> - <argument name="customStore" value="customStoreENNotUnique"/> - </actionGroup> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewNl"> - <argument name="customStore" value="customStoreNLNotUnique"/> - </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewEn"> @@ -72,11 +79,11 @@ <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> <argument name="productName" value="productformagetwo68980"/> </actionGroup> @@ -130,8 +137,16 @@ <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> <magentoCLI command="cache:flush" stepKey="cleanCache1"/> - + <createData entity="ApiCategory" stepKey="createCategory"> + <field key="name">category-admin</field> + </createData> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteENStoreViewIfExists"> + <argument name="storeViewName" value="{{customStoreENNotUnique.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteNLStoreViewIfExists"> + <argument name="storeViewName" value="{{customStoreNLNotUnique.name}}"/> + </actionGroup> <!-- Create Store View EN --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> <argument name="customStore" value="customStoreENNotUnique"/> @@ -140,10 +155,6 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewNl"> <argument name="customStore" value="customStoreNLNotUnique"/> </actionGroup> - <createData entity="ApiCategory" stepKey="createCategory"> - <field key="name">category-admin</field> - </createData> - <!-- Set the configuration for Generate "category/product" URL Rewrites to No--> <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> @@ -152,17 +163,18 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteStoreViewEn"> + <argument name="storeViewName" value="{{customStoreENNotUnique.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewIfExistsActionGroup" stepKey="deleteStoreViewNl"> + <argument name="storeViewName" value="{{customStoreNLNotUnique.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearStoreGridFilters"/> <actionGroup ref="deleteProductByName" stepKey="deleteImportedProduct"> <argument name="sku" value="productformagetwo68980"/> <argument name="name" value="productformagetwo68980"/> </actionGroup> <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFiltersIfSet"/> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> - <argument name="customStore" value="customStoreENNotUnique"/> - </actionGroup> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewNl"> - <argument name="customStore" value="customStoreNLNotUnique"/> - </actionGroup> <actionGroup ref="logout" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> @@ -194,11 +206,11 @@ <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> + <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> <argument name="productName" value="productformagetwo68980"/> </actionGroup> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml index a7a7c0c73d826..85e9d7847d5ea 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml @@ -29,7 +29,7 @@ <!--Open Url Rewrite Index Page and update the Custom Url Rewrite, Store, Request Path, Redirect Type and Description --> <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> <argument name="category" value="$$category.name$$"/> - <argument name="customUrlRewriteValue" value="For Category'"/> + <argument name="customUrlRewriteValue" value="For Category"/> <argument name="storeValue" value="Default Store View"/> <argument name="requestPath" value="newrequestpath.html"/> <argument name="redirectTypeValue" value="No"/> @@ -49,4 +49,4 @@ <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> </actionGroup> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml index 974550bb92214..477742e7e3618 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -29,7 +29,7 @@ <!--Open Url Rewrite Index Page and update the Custom Url Rewrite, Store, Request Path, Redirect Type and Description --> <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> <argument name="category" value="$$category.name$$"/> - <argument name="customUrlRewriteValue" value="For Category'"/> + <argument name="customUrlRewriteValue" value="For Category"/> <argument name="storeValue" value="Default Store View"/> <argument name="requestPath" value="newrequestpath.html"/> <argument name="redirectTypeValue" value="Permanent (301)"/> @@ -49,4 +49,4 @@ <argument name="newRequestPath" value="newrequestpath.html"/> </actionGroup> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml index c64019ea38acc..2367c3d982d15 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml @@ -29,7 +29,7 @@ <!--Open Url Rewrite Index Page and update the Custom Url Rewrite, Store, Request Path, Redirect Type and Description --> <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> <argument name="category" value="$$category.name$$"/> - <argument name="customUrlRewriteValue" value="For Category'"/> + <argument name="customUrlRewriteValue" value="For Category"/> <argument name="storeValue" value="Default Store View"/> <argument name="requestPath" value="newrequestpath.html"/> <argument name="redirectTypeValue" value="Temporary (302)"/> @@ -49,4 +49,4 @@ <argument name="newRequestPath" value="newrequestpath.html"/> </actionGroup> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml new file mode 100644 index 0000000000000..5a05dab7aa707 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest.xml @@ -0,0 +1,54 @@ +<?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="AdminDeleteCategoryUrlRewriteHypenAsRequestPathTest"> + <annotations> + <features value="UrlRewrite" /> + <stories value="Delete custom URL rewrite"/> + <title value="Delete category URL rewrite, hyphen as request path"/> + <description value="Delete category URL rewrite, hyphen as request path"/> + <testCaseId value="MC-5348" /> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create the Category Url Rewrite--> + <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> + <argument name="category" value="$$category.name$$"/> + <argument name="customUrlRewriteValue" value="For Category"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="-"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Delete the Category Url Rewrite--> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="-"/> + </actionGroup> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="You deleted the URL rewrite."/> + </actionGroup> + + <!--Verify AssertPageByUrlRewriteIsNotFound--> + <actionGroup ref="AssertPageByUrlRewriteIsNotFound" stepKey="checkUrlOnFrontend"> + <argument name="requestPath" value="-"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml new file mode 100644 index 0000000000000..e94e10767c632 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCategoryUrlRewriteWithRequestPathTest.xml @@ -0,0 +1,54 @@ +<?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="AdminDeleteCategoryUrlRewriteWithRequestPathTest"> + <annotations> + <features value="UrlRewrite" /> + <stories value="Delete custom URL rewrite"/> + <title value="Delete category URL rewrite, with request path"/> + <description value="Delete category URL rewrite, with request path"/> + <testCaseId value="MC-5349" /> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create the Category Url Rewrite--> + <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewriteSecondTime"> + <argument name="category" value="$$category.name$$"/> + <argument name="customUrlRewriteValue" value="For Category"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="newrequestpath.html"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Delete the Category Url Rewrite--> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewriteSecondTime"> + <argument name="requestPath" value="newrequestpath.html"/> + </actionGroup> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessageSecondTime"> + <argument name="message" value="You deleted the URL rewrite."/> + </actionGroup> + + <!--Verify AssertPageByUrlRewriteIsNotFound--> + <actionGroup ref="AssertPageByUrlRewriteIsNotFound" stepKey="checkUrlOnFrontendSecondTime"> + <argument name="requestPath" value="newrequestpath.html"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml new file mode 100644 index 0000000000000..c6ee1a7da9602 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml @@ -0,0 +1,114 @@ +<?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="AdminProductCreateUrlRewriteForCustomStoreViewTest"> + <annotations> + <features value="UrlRewrite"/> + <stories value="Create Product"/> + <title value="Product custom URL Key is preserved when assigned to a Category"/> + <description value="Verify Product custom URL Key (for custom Store View) is preserved when assigned to a Category (with custom URL Key) alongside with another Product without custom URL Key"/> + <testCaseId value="MC-6463"/> + <severity value="MAJOR"/> + <group value="catalog"/> + <group value="url_rewrite"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory" /> + </createData> + <createData entity="SimpleProduct" stepKey="createProductForUrlRewrite"> + <requiredEntity createDataKey="createCategory" /> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <magentoCLI command="indexer:reindex" stepKey="runReindex"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + <deleteData createDataKey="createProductForUrlRewrite" stepKey="deleteProductForUrlRewrite" /> + <deleteData createDataKey="createCategory" stepKey="deleteCategory" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearFilterForStores"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Step 1. Navigate as Admin on Product Page for edit product`s Url Key--> + <actionGroup ref="navigateToCreatedProductEditPage" stepKey="goToProductForUrlRewrite"> + <argument name="product" value="$$createProductForUrlRewrite$$"/> + </actionGroup> + <!--Step 2. As Admin switch on Custom Store View from Precondition --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToCustomStore"> + <argument name="storeView" value="customStore.name"/> + </actionGroup> + <!--Step 3. Set custom URL Key for product on Custom StoreView--> + <actionGroup ref="AdminProductFormUpdateUrlKeyActionGroup" stepKey="updateUrlKeyForProduct"> + <argument name="newUrlKey" value="U2"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductWithNewUrl"/> + <!--Step 4. Set URL Key for created category --> + <actionGroup ref="navigateToCreatedCategory" stepKey="navigateToCreatedSubCategory"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="ChangeSeoUrlKey" stepKey="updateUrlKeyForCategory"> + <argument name="value" value="U1"/> + </actionGroup> + <!--Step 5. On Storefront Assert what URL Key for Category is changed and is correct as for Default Store View --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="onCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="assertUrlCategoryOnDefaultStore"> + <argument name="category" value="$$createCategory.name$$"/> + <argument name="newRequestPath" value="u1.html"/> + </actionGroup> + <!--Step 6. On Storefront Assert what URL Key for product is correct(as initial URL) --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="navigateToProductInDefaultStore"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductUrl"> + <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!--Step 7. On Storefront Assert what URL Key for product is correct for Default Store View (as initial URL) --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="navigateToProductForUrlRewriteInDefaultStore"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createProductForUrlRewrite$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductWithChangedUrl"> + <argument name="productUrl" value="$$createProductForUrlRewrite.custom_attributes[url_key]$$"/> + </actionGroup> + <!--Step 8. On Storefront switch on created Custom Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreViewOnStorefront"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <!--Step 9. On Storefront Assert what URL Key for Category is changed and is correct for Custom Store View --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="assertUrlCategoryOnCustomStore"> + <argument name="category" value="$$createCategory.name$$"/> + <argument name="newRequestPath" value="u1.html"/> + </actionGroup> + <!--Step 10. On Storefront Assert what URL Key for product is correct for Custom Store View (as initial URL) --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="navigateToProductInCustomStore"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductUrlOnCustomStore"> + <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!--Step 11. On Storefront Assert what URL Key for product is changed and is correct for Custom Store View --> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="assertProductUrlRewriteInStoreFront"> + <argument name="productName" value="$$createProductForUrlRewrite.name$$"/> + <argument name="productSku" value="$$createProductForUrlRewrite.sku$$"/> + <argument name="productRequestPath" value="u2.html"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml new file mode 100644 index 0000000000000..81dedfea7a35e --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithNoRedirectTest.xml @@ -0,0 +1,89 @@ +<?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="AdminUpdateCmsPageRewriteEntityWithNoRedirectTest"> + <annotations> + <stories value="Update CMS Page URL Redirect With No Redirect"/> + <title value="Update CMS Page URL Redirect With No Redirect"/> + <description value="Login as Admin and tried to update the created URL Rewrite for CMS page"/> + <group value="cMSContent"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="simpleCmsPage" stepKey="createCMSPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCMSPage" stepKey="deleteCMSPage"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create Custom Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + + <!-- Open CMS Edit Page, Get the CMS ID and Modify Store View Option to All Store Views --> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="cmsId" regex="#\/([0-9]*)?\/$#"/> + <actionGroup ref="AddStoreViewToCmsPage" stepKey="updateStoreViewForCmsPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + <argument name="storeViewName" value="All Store Views"/> + </actionGroup> + + <!--Create CMS Page URL Redirect--> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="created-new-cms-page"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + <argument name="description" value="Created New CMS Page"/> + </actionGroup> + + <!--Search created CMS page url rewrite in grid--> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="searchUrlRewrite"> + <argument name="requestPath" value="created-new-cms-page"/> + </actionGroup> + + <!-- Update URL Rewrite for CMS Page --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateUrlRewriteFirstAttempt"> + <argument name="storeValue" value="{{customStore.name}}"/> + <argument name="requestPath" value="newrequestpath"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="description" value="test_description_custom_store"/> + </actionGroup> + + <!-- Assert Url Rewrite Save Message --> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="The URL Rewrite has been saved."/> + </actionGroup> + + <!-- Assert Url Rewrite Cms Page Redirect --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchToCustomStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront"> + <argument name="page" value="newrequestpath"/> + </actionGroup> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="newrequestpath"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"/> + + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml new file mode 100644 index 0000000000000..f073794896c2c --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithPermanentReirectTest.xml @@ -0,0 +1,85 @@ +<?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="AdminUpdateCmsPageRewriteEntityWithPermanentRedirectTest"> + <annotations> + <stories value="Update CMS Page URL Redirect With Permanent Redirect"/> + <title value="Update CMS Page URL Redirect With Permanent Redirect"/> + <description value="Login as Admin and tried to update the created URL Rewrite for CMS page"/> + <group value="cMSContent"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="simpleCmsPage" stepKey="createCMSPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCMSPage" stepKey="deleteCMSPage"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create Custom Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + + <!-- Open CMS Edit Page, Get the CMS ID and Modify Store View Option to All Store Views --> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="cmsId" regex="#\/([0-9]*)?\/$#"/> + <actionGroup ref="AddStoreViewToCmsPage" stepKey="updateStoreViewForCmsPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + <argument name="storeViewName" value="All Store Views"/> + </actionGroup> + + <!--Create CMS Page URL Redirect--> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="created-new-cms-page"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + <argument name="description" value="Created New CMS Page"/> + </actionGroup> + + <!--Search created CMS page url rewrite in grid--> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="searchUrlRewrite"> + <argument name="requestPath" value="created-new-cms-page"/> + </actionGroup> + + <!-- Update URL Rewrite for CMS Page --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="permanentrequestpath.htm"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="description" value="test_description_301"/> + </actionGroup> + + <!-- Assert Url Rewrite Save Message --> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="The URL Rewrite has been saved."/> + </actionGroup> + + <!-- Assert Url Rewrite Cms Page Redirect --> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront"> + <argument name="page" value="permanentrequestpath.htm"/> + </actionGroup> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="permanentrequestpath.htm"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"/> + + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..8f04fe7cf9ab9 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest.xml @@ -0,0 +1,85 @@ +<?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="AdminUpdateCmsPageRewriteEntityWithTemporaryRedirectTest"> + <annotations> + <stories value="Update CMS Page URL Redirect With Temporary Redirect"/> + <title value="Update CMS Page URL Redirect With Temporary Redirect"/> + <description value="Login as Admin and tried to update the created URL Rewrite for CMS page"/> + <group value="cMSContent"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="simpleCmsPage" stepKey="createCMSPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCMSPage" stepKey="deleteCMSPage"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create Custom Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + + <!-- Open CMS Edit Page, Get the CMS ID and Modify Store View Option to All Store Views --> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="cmsId" regex="#\/([0-9]*)?\/$#"/> + <actionGroup ref="AddStoreViewToCmsPage" stepKey="updateStoreViewForCmsPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + <argument name="storeViewName" value="All Store Views"/> + </actionGroup> + + <!--Create CMS Page URL Redirect--> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="created-new-cms-page"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + <argument name="description" value="Created New CMS Page"/> + </actionGroup> + + <!--Search created CMS page url rewrite in grid--> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="searchUrlRewrite"> + <argument name="requestPath" value="created-new-cms-page"/> + </actionGroup> + + <!-- Update URL Rewrite for CMS Page --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="temporaryrequestpath.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="test description_302"/> + </actionGroup> + + <!-- Assert Url Rewrite Save Message --> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="The URL Rewrite has been saved."/> + </actionGroup> + + <!-- Assert Url Rewrite Cms Page Redirect --> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront"> + <argument name="page" value="temporaryrequestpath.html"/> + </actionGroup> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="temporaryrequestpath.html"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"/> + + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml new file mode 100644 index 0000000000000..a3d3b897ef75d --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml @@ -0,0 +1,85 @@ +<?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="AdminUrlRewritesForProductAfterImportTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Different number of URL rewrites when editing or importing a product"/> + <title value="Verify the number of URL rewrites when edit or import product"/> + <description value="After importing products to admin verify the number of URL including categories matches"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20229"/> + <group value="urlRewrite"/> + </annotations> + <before> + <comment userInput="Set the configuration for Generate category/product URL Rewrites" stepKey="commentSetURLRewriteConfiguration" /> + <comment userInput="Enable config to generate category/product URL Rewrites " stepKey="commentEnableConfig" /> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> + <createData entity="NewRootCategory" stepKey="simpleSubCategory1"> + <field key="parent_id">2</field> + </createData> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> + <requiredEntity createDataKey="simpleSubCategory1"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory3"> + <requiredEntity createDataKey="simpleSubCategory2"/> + </createData> + <comment userInput="Create Simple product 1 and assign it to Category 3 " stepKey="commentCreateSimpleProduct" /> + <createData entity="SimpleProductAfterImport1" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="simpleSubCategory3"/> + </createData> + </before> + <after> + <comment userInput="Delete all products that replaced products in the before block post import " stepKey="commentDeleteAllProducts" /> + <deleteData stepKey="deleteSimpleProduct1" url="/V1/products/SimpleProductForTest1"/> + <deleteData createDataKey="simpleSubCategory3" stepKey="deleteSimpleSubCategory3"/> + <deleteData createDataKey="simpleSubCategory2" stepKey="deleteSimpleSubCategory2"/> + <deleteData createDataKey="simpleSubCategory1" stepKey="deleteSimpleSubCategory1"/> + <comment userInput="Disable config to generate category/product URL Rewrites " stepKey="commentDisableConfig" /> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + + <comment userInput="1. Log in to Admin " stepKey="commentAdminLogin" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <comment userInput="2. Open Marketing - SEO and Search - URL Rewrites " stepKey="commentVerifyUrlRewrite" /> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> + + <comment userInput="3. Import products with add/update behavior " stepKey="commentProductImport" /> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="catalog_import_products_url_rewrite.csv"/> + <argument name="importNoticeMessage" value="Created: 0, Updated: 1, Deleted: 0"/> + </actionGroup> + + <comment userInput="4. Assert Simple Product1 on grid " stepKey="commentVerifyProduct" /> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct1OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <comment userInput="5. Open Marketing - SEO and Search - URL Rewrites" stepKey="commentVerifyURLAfterImport" /> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$createSimpleProduct.custom_attributes[url_key]$-new.html" stepKey="inputProductName2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue6"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue7"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Unit/Controller/RouterTest.php b/app/code/Magento/UrlRewrite/Test/Unit/Controller/RouterTest.php index 6eea8b962bf9f..c935b3c7ec4cb 100644 --- a/app/code/Magento/UrlRewrite/Test/Unit/Controller/RouterTest.php +++ b/app/code/Magento/UrlRewrite/Test/Unit/Controller/RouterTest.php @@ -351,54 +351,4 @@ public function testMatch() $this->router->match($this->request); } - - /** - * Test to match corresponding URL Rewrite on request with query params - * - * @param string $originalRequestPath - * @param string $requestPath - * @param int $countOfQueryParams - * @dataProvider matchWithQueryParamsDataProvider - */ - public function testMatchWithQueryParams(string $originalRequestPath, string $requestPath, int $countOfQueryParams) - { - $targetPath = 'target-path'; - - $this->storeManager->method('getStore')->willReturn($this->store); - $urlRewrite = $this->createMock(UrlRewrite::class); - $urlRewrite->method('getRedirectType')->willReturn(0); - $urlRewrite->method('getTargetPath')->willReturn($targetPath); - $urlRewrite->method('getRequestPath')->willReturn($requestPath); - $this->urlFinder->method('findOneByData') - ->with([UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::STORE_ID => $this->store->getId()]) - ->willReturn($urlRewrite); - - $this->requestQuery->method('count')->willReturn($countOfQueryParams); - $this->request->method('getPathInfo') - ->willReturn($originalRequestPath); - $this->request->expects($this->once()) - ->method('setPathInfo') - ->with('/' . $targetPath); - $this->request->expects($this->once()) - ->method('setAlias') - ->with(UrlInterface::REWRITE_REQUEST_PATH_ALIAS, $requestPath); - $this->actionFactory->expects($this->once()) - ->method('create') - ->with(Forward::class); - - $this->router->match($this->request); - } - - /** - * Data provider for Test to match corresponding URL Rewrite on request with query params - * - * @return array - */ - public function matchWithQueryParamsDataProvider(): array - { - return [ - ['/category.html/', 'category.html/', 0], - ['/category.html/', 'category.html', 1], - ]; - } } diff --git a/app/code/Magento/UrlRewrite/etc/db_schema.xml b/app/code/Magento/UrlRewrite/etc/db_schema.xml index 6e0014873202d..93e84d8e02a0f 100644 --- a/app/code/Magento/UrlRewrite/etc/db_schema.xml +++ b/app/code/Magento/UrlRewrite/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="url_rewrite" resource="default" engine="innodb" comment="Url Rewrites"> <column xsi:type="int" name="url_rewrite_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rewrite Id"/> + comment="Rewrite ID"/> <column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Entity type code"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -18,7 +18,7 @@ <column xsi:type="smallint" name="redirect_type" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Redirect Type"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Description"/> <column xsi:type="smallint" name="is_autogenerated" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is rewrite generated automatically flag"/> @@ -37,5 +37,8 @@ <column name="store_id"/> <column name="entity_id"/> </index> + <index referenceId="URL_REWRITE_ENTITY_ID" indexType="btree"> + <column name="entity_id"/> + </index> </table> </schema> diff --git a/app/code/Magento/UrlRewrite/etc/db_schema_whitelist.json b/app/code/Magento/UrlRewrite/etc/db_schema_whitelist.json index bdaed647587a6..658673959a734 100644 --- a/app/code/Magento/UrlRewrite/etc/db_schema_whitelist.json +++ b/app/code/Magento/UrlRewrite/etc/db_schema_whitelist.json @@ -14,11 +14,12 @@ }, "index": { "URL_REWRITE_TARGET_PATH": true, - "URL_REWRITE_STORE_ID_ENTITY_ID": true + "URL_REWRITE_STORE_ID_ENTITY_ID": true, + "URL_REWRITE_ENTITY_ID": true }, "constraint": { "PRIMARY": true, "URL_REWRITE_REQUEST_PATH_STORE_ID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 0acece9271f7c..e6b03755bea47 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -31,6 +31,11 @@ class EntityUrl implements ResolverInterface */ private $customUrlLocator; + /** + * @var int + */ + private $redirectType; + /** * @param UrlFinderInterface $urlFinder * @param CustomUrlLocatorInterface $customUrlLocator @@ -57,49 +62,83 @@ public function resolve( throw new GraphQlInputException(__('"url" argument should be specified and not empty')); } + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; $url = $args['url']; if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); } + $this->redirectType = 0; $customUrl = $this->customUrlLocator->locateUrl($url); $url = $customUrl ?: $url; - $urlRewrite = $this->findCanonicalUrl($url, (int)$context->getExtensionAttributes()->getStore()->getId()); - if ($urlRewrite) { - if (!$urlRewrite->getEntityId()) { + $finalUrlRewrite = $this->findFinalUrl($url, $storeId); + if ($finalUrlRewrite) { + $relativeUrl = $finalUrlRewrite->getRequestPath(); + $resultArray = $this->rewriteCustomUrls($finalUrlRewrite, $storeId) ?? [ + 'id' => $finalUrlRewrite->getEntityId(), + 'canonical_url' => $relativeUrl, + 'relative_url' => $relativeUrl, + 'redirectCode' => $this->redirectType, + 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) + ]; + + if (empty($resultArray['id'])) { throw new GraphQlNoSuchEntityException( __('No such entity found with matching URL key: %url', ['url' => $url]) ); } - $result = [ - 'id' => $urlRewrite->getEntityId(), - 'canonical_url' => $urlRewrite->getTargetPath(), - 'relative_url' => $urlRewrite->getTargetPath(), - 'type' => $this->sanitizeType($urlRewrite->getEntityType()) - ]; + + $result = $resultArray; } return $result; } /** - * Find the canonical url passing through all redirects if any + * Handle custom urls with and without redirects + * + * @param UrlRewrite $finalUrlRewrite + * @param int $storeId + * @return array|null + */ + private function rewriteCustomUrls(UrlRewrite $finalUrlRewrite, int $storeId): ?array + { + if ($finalUrlRewrite->getEntityType() === 'custom' || !($finalUrlRewrite->getEntityId() > 0)) { + $finalCustomUrlRewrite = clone $finalUrlRewrite; + $finalUrlRewrite = $this->findFinalUrl($finalCustomUrlRewrite->getTargetPath(), $storeId, true); + $relativeUrl = + $finalCustomUrlRewrite->getRedirectType() == 0 + ? $finalCustomUrlRewrite->getRequestPath() : $finalUrlRewrite->getRequestPath(); + return [ + 'id' => $finalUrlRewrite->getEntityId(), + 'canonical_url' => $relativeUrl, + 'relative_url' => $relativeUrl, + 'redirectCode' => $finalCustomUrlRewrite->getRedirectType(), + 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) + ]; + } + return null; + } + + /** + * Find the final url passing through all redirects if any * * @param string $requestPath * @param int $storeId + * @param bool $findCustom * @return UrlRewrite|null */ - private function findCanonicalUrl(string $requestPath, int $storeId) : ?UrlRewrite + private function findFinalUrl(string $requestPath, int $storeId, bool $findCustom = false): ?UrlRewrite { $urlRewrite = $this->findUrlFromRequestPath($requestPath, $storeId); - if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + if ($urlRewrite) { + $this->redirectType = $urlRewrite->getRedirectType(); while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath(), $storeId); } - } - if (!$urlRewrite) { + } else { $urlRewrite = $this->findUrlFromTargetPath($requestPath, $storeId); } - if ($urlRewrite && !$urlRewrite->getEntityId() && !$urlRewrite->getIsAutogenerated()) { + if ($urlRewrite && ($findCustom && !$urlRewrite->getEntityId() && !$urlRewrite->getIsAutogenerated())) { $urlRewrite = $this->findUrlFromTargetPath($urlRewrite->getTargetPath(), $storeId); } @@ -113,7 +152,7 @@ private function findCanonicalUrl(string $requestPath, int $storeId) : ?UrlRewri * @param int $storeId * @return UrlRewrite|null */ - private function findUrlFromRequestPath(string $requestPath, int $storeId) : ?UrlRewrite + private function findUrlFromRequestPath(string $requestPath, int $storeId): ?UrlRewrite { return $this->urlFinder->findOneByData( [ @@ -130,7 +169,7 @@ private function findUrlFromRequestPath(string $requestPath, int $storeId) : ?Ur * @param int $storeId * @return UrlRewrite|null */ - private function findUrlFromTargetPath(string $targetPath, int $storeId) : ?UrlRewrite + private function findUrlFromTargetPath(string $targetPath, int $storeId): ?UrlRewrite { return $this->urlFinder->findOneByData( [ diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php index fb7bbd634d11f..55cd505928f42 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php @@ -11,9 +11,11 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\Model\AbstractModel; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDTO; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\EntityManager\TypeResolver; +use Magento\Framework\EntityManager\MetadataPool; /** * Returns URL rewrites list for the specified product @@ -25,13 +27,37 @@ class UrlRewrite implements ResolverInterface */ private $urlFinder; + /** + * @var array + */ + private $entityTypeMapping; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var TypeResolver + */ + private $typeResolver; + /** * @param UrlFinderInterface $urlFinder + * @param TypeResolver $typeResolver + * @param MetadataPool $metadataPool + * @param array $entityTypeMapping */ public function __construct( - UrlFinderInterface $urlFinder + UrlFinderInterface $urlFinder, + TypeResolver $typeResolver, + MetadataPool $metadataPool, + array $entityTypeMapping = [] ) { $this->urlFinder = $urlFinder; + $this->typeResolver = $typeResolver; + $this->metadataPool = $metadataPool; + $this->entityTypeMapping = $entityTypeMapping; } /** @@ -48,11 +74,24 @@ public function resolve( throw new LocalizedException(__('"model" value should be specified')); } - /** @var AbstractModel $entity */ + /** @var AbstractModel $entity */ $entity = $value['model']; $entityId = $entity->getEntityId(); - $urlRewriteCollection = $this->urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $entityId]); + $resolveEntityType = $this->typeResolver->resolve($entity); + $metadata = $this->metadataPool->getMetadata($resolveEntityType); + $entityType = $this->getEntityType($metadata->getEavEntityType()); + + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + + $data = [ + UrlRewriteDTO::ENTITY_TYPE => $entityType, + UrlRewriteDTO::ENTITY_ID => $entityId, + UrlRewriteDTO::STORE_ID => $storeId + ]; + + $urlRewriteCollection = $this->urlFinder->findAllByData($data); + $urlRewrites = []; /** @var UrlRewriteDTO $urlRewrite */ @@ -80,14 +119,32 @@ private function getUrlParameters(string $targetPath): array { $urlParameters = []; $targetPathParts = explode('/', trim($targetPath, '/')); + $count = count($targetPathParts) - 1; - for ($i = 3; ($i < sizeof($targetPathParts) - 1); $i += 2) { + /** $index starts from 3 to eliminate catalog/product/view/ part and fetch only name, + value data from from target path */ + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($index = 3; $index < $count; $index += 2) { $urlParameters[] = [ - 'name' => $targetPathParts[$i], - 'value' => $targetPathParts[$i + 1] + 'name' => $targetPathParts[$index], + 'value' => $targetPathParts[$index + 1] ]; } - return $urlParameters; } + + /** + * Get the entity type + * + * @param string $entityTypeMetadata + * @return string + */ + private function getEntityType(string $entityTypeMetadata) : string + { + $entityType = ''; + if ($entityTypeMetadata) { + $entityType = $this->entityTypeMapping[$entityTypeMetadata]; + } + return $entityType; + } } diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index 92d237d3f01e1..7f7ebb627b4dc 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -2,13 +2,14 @@ # See COPYING.txt for license details. type Query { - urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") @cache(cacheIdentity: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite\\UrlResolverIdentity") + urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page, using as input a url_key appended by the url_suffix, if one exists") @cache(cacheIdentity: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite\\UrlResolverIdentity") } type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") canonical_url: String @deprecated(reason: "The canonical_url field is deprecated, use relative_url instead.") relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") + redirectCode: Int @doc(description: "301 or 302 HTTP code for url permanent or temporary redirect or 0 for the 200 no redirect") type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") } diff --git a/app/code/Magento/User/Block/Role/Tab/Info.php b/app/code/Magento/User/Block/Role/Tab/Info.php index 8a656efa97443..1b8b528aff774 100644 --- a/app/code/Magento/User/Block/Role/Tab/Info.php +++ b/app/code/Magento/User/Block/Role/Tab/Info.php @@ -6,7 +6,9 @@ namespace Magento\User\Block\Role\Tab; /** - * implementing now + * Info + * + * User role tab info * * @SuppressWarnings(PHPMD.DepthOfInheritance) */ @@ -18,6 +20,8 @@ class Info extends \Magento\Backend\Block\Widget\Form\Generic implements \Magent const IDENTITY_VERIFICATION_PASSWORD_FIELD = 'current_password'; /** + * Get tab label + * * @return \Magento\Framework\Phrase */ public function getTabLabel() @@ -26,6 +30,8 @@ public function getTabLabel() } /** + * Get tab title + * * @return string */ public function getTabTitle() @@ -34,6 +40,8 @@ public function getTabTitle() } /** + * Can show tab + * * @return bool */ public function canShowTab() @@ -42,6 +50,8 @@ public function canShowTab() } /** + * Is tab hidden + * * @return bool */ public function isHidden() @@ -50,6 +60,8 @@ public function isHidden() } /** + * Before html rendering + * * @return $this */ public function _beforeToHtml() @@ -60,6 +72,8 @@ public function _beforeToHtml() } /** + * Form initializatiion + * * @return void */ protected function _initForm() @@ -99,7 +113,7 @@ protected function _initForm() 'label' => __('Your Password'), 'id' => self::IDENTITY_VERIFICATION_PASSWORD_FIELD, 'title' => __('Your Password'), - 'class' => 'input-text validate-current-password required-entry', + 'class' => 'validate-current-password required-entry', 'required' => true ] ); diff --git a/app/code/Magento/User/Block/User/Edit/Tab/Main.php b/app/code/Magento/User/Block/User/Edit/Tab/Main.php index 27e00483733d0..3182393db8eaf 100644 --- a/app/code/Magento/User/Block/User/Edit/Tab/Main.php +++ b/app/code/Magento/User/Block/User/Edit/Tab/Main.php @@ -184,7 +184,7 @@ protected function _prepareForm() 'label' => __('Your Password'), 'id' => self::CURRENT_USER_PASSWORD_FIELD, 'title' => __('Your Password'), - 'class' => 'input-text validate-current-password required-entry', + 'class' => 'validate-current-password required-entry', 'required' => true ] ); diff --git a/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php b/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php index 6d921dfdcdd65..93b5389bc3608 100644 --- a/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php +++ b/app/code/Magento/User/Model/Backend/Config/ObserverConfig.php @@ -4,13 +4,37 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\User\Model\Backend\Config; /** * User backend observer helper class + * + * Class \Magento\User\Model\Backend\Config\ObserverConfig */ class ObserverConfig { + /** + * Config path for lockout threshold + */ + private const XML_ADMIN_SECURITY_LOCKOUT_THRESHOLD = 'admin/security/lockout_threshold'; + + /** + * Config path for password change is forced or not + */ + private const XML_ADMIN_SECURITY_PASSWORD_IS_FORCED = 'admin/security/password_is_forced'; + + /** + * Config path for password lifetime + */ + private const XML_ADMIN_SECURITY_PASSWORD_LIFETIME = 'admin/security/password_lifetime'; + + /** + * Config path for maximum lockout failures + */ + private const XML_ADMIN_SECURITY_LOCKOUT_FAILURES = 'admin/security/lockout_failures'; + /** * Backend configuration interface * @@ -19,6 +43,8 @@ class ObserverConfig protected $backendConfig; /** + * Constructor + * * @param \Magento\Backend\App\ConfigInterface $backendConfig */ public function __construct( @@ -44,11 +70,12 @@ public function _isLatestPasswordExpired($latestPassword) /** * Get admin lock threshold from configuration + * * @return int */ public function getAdminLockThreshold() { - return 60 * (int)$this->backendConfig->getValue('admin/security/lockout_threshold'); + return 60 * (int)$this->backendConfig->getValue(self::XML_ADMIN_SECURITY_LOCKOUT_THRESHOLD); } /** @@ -58,7 +85,7 @@ public function getAdminLockThreshold() */ public function isPasswordChangeForced() { - return (bool)(int)$this->backendConfig->getValue('admin/security/password_is_forced'); + return (bool)(int)$this->backendConfig->getValue(self::XML_ADMIN_SECURITY_PASSWORD_IS_FORCED); } /** @@ -68,7 +95,7 @@ public function isPasswordChangeForced() */ public function getAdminPasswordLifetime() { - return 86400 * (int)$this->backendConfig->getValue('admin/security/password_lifetime'); + return 86400 * (int)$this->backendConfig->getValue(self::XML_ADMIN_SECURITY_PASSWORD_LIFETIME); } /** @@ -78,6 +105,6 @@ public function getAdminPasswordLifetime() */ public function getMaxFailures() { - return (int)$this->backendConfig->getValue('admin/security/lockout_failures'); + return (int)$this->backendConfig->getValue(self::XML_ADMIN_SECURITY_LOCKOUT_FAILURES); } } diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index d79f2013241e6..3b41d529b542b 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -665,6 +665,10 @@ public function loadByUsername($username) { $data = $this->getResource()->loadByUsername($username); if ($data !== false) { + if (is_string($data['extra'])) { + $data['extra'] = $this->serializer->unserialize($data['extra']); + } + $this->setData($data); $this->setOrigData(); } diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminAddNewUserRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminAddNewUserRoleActionGroup.xml index 76a83f5d5a5aa..175f6203350c7 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminAddNewUserRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminAddNewUserRoleActionGroup.xml @@ -36,7 +36,7 @@ <selectOption userInput="{{role.resourceAccess}}" selector="{{AdminCreateRoleSection.roleResourceNew}}" stepKey="selectResourceAccess"/> <click selector="{{AdminCreateRoleSection.save}}" stepKey="saveUserRole"/> <waitForPageLoad stepKey="waitForSaving"/> - <see userInput="You saved the role." selector="{{AdminMessagesSection.successMessage}}" stepKey="seeMessage"/> + <see userInput="You saved the role." selector="{{AdminMessagesSection.success}}" stepKey="seeMessage"/> </actionGroup> <actionGroup name="AdminAddNewUserRoleWithCustomRoleScopes" extends="AdminAddNewUserRoleActionGroup"> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserWithRoleAndIsActiveActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserWithRoleAndIsActiveActionGroup.xml new file mode 100644 index 0000000000000..779dcbc512fd2 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserWithRoleAndIsActiveActionGroup.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"> + <!--Create new user with role and active setting--> + <actionGroup name="AdminCreateUserWithRoleAndIsActiveActionGroup" extends="AdminCreateUserWithRoleActionGroup"> + <checkOption selector="{{AdminNewUserFormSection.userIsActive(user.is_active)}}" stepKey="checkIsActive" after="confirmPassword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserRoleActionGroup.xml index d2c881b771973..84c9e26eed54c 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserRoleActionGroup.xml @@ -21,7 +21,7 @@ <click selector="{{AdminEditRoleInfoSection.deleteButton}}" stepKey="deleteUserRole"/> <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForConfirmModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> - <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitSuccessMessage"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the role." stepKey="seeUserRoleDeleteMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the role." stepKey="seeUserRoleDeleteMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillUserRoleFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillUserRoleFormActionGroup.xml index 480695aa7a931..7b913382651ae 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillUserRoleFormActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillUserRoleFormActionGroup.xml @@ -17,13 +17,13 @@ <argument name="currentAdminPassword" type="string" defaultValue="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" /> </arguments> - <fillField selector="{{AdminCreateRoleSection.name}}" userInput="{{role.name}}" stepKey="fillRoleName"/> + <fillField selector="{{AdminCreateRoleSection.name}}" userInput="{{role.rolename}}" stepKey="fillRoleName"/> <fillField selector="{{AdminCreateRoleSection.password}}" userInput="{{currentAdminPassword}}" stepKey="fillCurrentUserPassword"/> <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> <waitForPageLoad stepKey="waitForRoleResourceTab" /> <selectOption userInput="{{role.resourceAccess}}" selector="{{AdminCreateRoleSection.resourceAccess}}" stepKey="selectResourceAccess" /> - <performOn stepKey="checkNeededResources" selector="{{AdminCreateRoleSection.resourceTree}}" function="function($I,$userRoles={{role.resources}}){foreach($userRoles as $userRole){$I->conditionalClick('//li[@data-id=\'' . $userRole . '\']//*[@class=\'jstree-checkbox\']','//li[@data-id=\'' . $userRole . '\' and contains(@class, \'jstree-checked\')]',false);}}" /> + <performOn stepKey="checkNeededResources" selector="{{AdminCreateRoleSection.resourceTree}}" function="function($I,$userRoles={{role.resource}}){foreach($userRoles as $userRole){$I->conditionalClick('//li[@data-id=\'' . $userRole . '\']//*[@class=\'jstree-checkbox\']','//li[@data-id=\'' . $userRole . '\' and contains(@class, \'jstree-checked\')]',false);}}" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenAdminUsersPageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenAdminUsersPageActionGroup.xml new file mode 100644 index 0000000000000..1c4a35eeff6cb --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenAdminUsersPageActionGroup.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="AdminOpenAdminUsersPageActionGroup"> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="navigateToAdminUsersGrid"/> + <waitForPageLoad stepKey="waitForAdminUsersPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUpdateUserRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUpdateUserRoleActionGroup.xml new file mode 100644 index 0000000000000..9c632e70db55d --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUpdateUserRoleActionGroup.xml @@ -0,0 +1,26 @@ +<?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="AdminUpdateUserRoleActionGroup"> + <arguments> + <argument name="role" type="entity"/> + </arguments> + <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForPageLoad stepKey="waitForPageScrollToTop" time="15"/> + <click selector="{{AdminNewUserFormSection.userRoleTab}}" stepKey="openUserRoleTab"/> + <waitForPageLoad stepKey="waitForUserRoleTabOpened" /> + <click selector="{{AdminNewUserFormSection.resetFilter}}" stepKey="resetGridFilter"/> + <waitForPageLoad stepKey="waitForFiltersReset"/> + <fillField selector="{{AdminNewUserFormSection.roleFilterField}}" userInput="{{role.name}}" stepKey="fillRoleFilterField"/> + <click selector="{{AdminNewUserFormSection.search}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForFiltersApplied"/> + <checkOption selector="{{AdminNewUserFormSection.roleRadiobutton(role.name)}}" stepKey="assignRole"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserIsInGridActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserIsInGridActionGroup.xml new file mode 100644 index 0000000000000..3499f4e0d951c --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserIsInGridActionGroup.xml @@ -0,0 +1,22 @@ +<?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="AssertAdminUserIsInGridActionGroup"> + <arguments> + <argument name="user" type="entity"/> + </arguments> + <click selector="{{AdminUserGridSection.resetButton}}" stepKey="resetGridFilter"/> + <waitForPageLoad stepKey="waitForFiltersReset" time="15"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="{{user.username}}" stepKey="enterUserName"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearch"/> + <waitForPageLoad stepKey="waitForGridToLoad" time="15"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="{{user.username}}" stepKey="seeUser"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AssertUserRoleRestrictedAccessActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertUserRoleRestrictedAccessActionGroup.xml new file mode 100644 index 0000000000000..0747eab31588e --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertUserRoleRestrictedAccessActionGroup.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="AssertUserRoleRestrictedAccessActionGroup"> + <see selector="{{AdminHeaderSection.pageHeading}}" userInput="Sorry, you need permissions to view this content." stepKey="seeErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/Data/UserData.xml b/app/code/Magento/User/Test/Mftf/Data/UserData.xml index d465851c62373..7947c8ee3c161 100644 --- a/app/code/Magento/User/Test/Mftf/Data/UserData.xml +++ b/app/code/Magento/User/Test/Mftf/Data/UserData.xml @@ -33,6 +33,12 @@ <item>1</item> </array> </entity> + <entity name="AdminUserWithUpdatedUserRoleToSales" extends="NewAdminUser"> + <data key="password">123123qA</data> + <data key="password_confirmation">123123qA</data> + <data key="role">{{roleSales.rolename}}</data> + </entity> + <entity name="EditAdminUser" type="user"> <data key="username" unique="suffix">admin</data> <data key="firstname">John</data> @@ -109,6 +115,36 @@ </array> </entity> + <entity name="activeAdmin" type="user" extends="NewAdminUser"> + <data key="name" unique="suffix">admin</data> + <data key="is_active">1</data> + </entity> + <entity name="inactiveAdmin" type="user" extends="NewAdminUser"> + <data key="name" unique="suffix">admin</data> + <data key="is_active">0</data> + </entity> + + <entity name="adminUserCorrectPassword" type="user"> + <data key="username">admin_user_with_correct_password</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="email" unique="prefix">admin@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="interface_local_label">English (United States)</data> + <data key="is_active">true</data> + <data key="is_active_label">Active</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="role">Administrators</data> + <array key="roles"> + <item>1</item> + </array> + </entity> + <entity name="adminUserIncorrectPassword" type="user"> + <data key="username">admin_user_with_correct_password</data> + <data key="password">123123123q</data> + </entity> <!-- Since User delete action is performed via POST request we created this entity to be able to delete it. Please use "AdminDeleteUserViaCurlActionGroup". diff --git a/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml index a0d89bbf3fb9d..a39e6cf47c295 100644 --- a/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml +++ b/app/code/Magento/User/Test/Mftf/Data/UserRoleData.xml @@ -10,38 +10,49 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="adminRole" type="role"> <data key="name" unique="suffix">adminRole</data> + <data key="rolename" unique="suffix">adminRole</data> <data key="scope">1</data> <data key="access">1</data> </entity> - <entity name="roleAdministrator" type="role"> + <entity name="roleAdministrator" type="user_role"> <data key="name" unique="suffix">Administrator </data> + <data key="rolename" unique="suffix">Administrator </data> <data key="resourceAccess">All</data> - <data key="resources">[]</data> + <data key="all">1</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="resource">[]</data> </entity> - <entity name="roleSales" type="role"> + <entity name="roleSales"> <data key="name" unique="suffix">Role Sales </data> + <data key="rolename" unique="suffix">Role Sales </data> <data key="resourceAccess">Custom</data> - <data key="resources">['Magento_Sales::sales','Magento_Sales::sales_operation','Magento_Sales::actions','Magento_Sales::sales_order','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_Backend::system','Magento_Backend::system_other_settings','Magento_AdminNotification::adminnotification','Magento_AdminNotification::show_list']</data> + <data key="all">0</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="resource">['Magento_Sales::sales','Magento_Sales::sales_operation','Magento_Sales::actions','Magento_Sales::sales_order','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_Backend::system_other_settings','Magento_AdminNotification::adminnotification','Magento_AdminNotification::show_list']</data> </entity> <entity name="limitedRole" type="role"> <data key="name" unique="suffix">Limited</data> + <data key="rolename" unique="suffix">Limited</data> <data key="roleScopes">Custom</data> <data key="resourceAccess">All</data> </entity> <entity name="restrictedRole" type="role"> <data key="name" unique="suffix">Restricted</data> + <data key="rolename" unique="suffix">Restricted</data> <data key="roleScopes">Custom</data> <data key="resourceAccess">All</data> </entity> <!-- This admin created for checking turn off "Bulk Actions" --> <entity name="adminWithoutBulkActionRole" type="user_role"> + <data key="name">restrictedWebsiteRole</data> <data key="rolename">restrictedWebsiteRole</data> - <data key="current_password">123123q</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="all">0</data> <data key="gws_is_all">0</data> <array key="gws_websites"> <item>1</item> @@ -78,4 +89,18 @@ <item>Magento_Backend::system</item> </array> </entity> + <entity name="adminProductInWebsiteRole" type="user_role"> + <data key="rolename" unique="suffix">restrictedWebsiteRole</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="all">0</data> + </entity> + <entity name="adminRestrictedProductRole" type="user_role"> + <data key="rolename" unique="suffix">restrictedCatalogRole</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="all">0</data> + </entity> + + <entity name="genericAdminRole" type="role"> + <data key="name">Administrators</data> + </entity> </entities> diff --git a/app/code/Magento/User/Test/Mftf/Metadata/user_role-meta.xml b/app/code/Magento/User/Test/Mftf/Metadata/user_role-meta.xml index 9d0132453c798..5384bd520b2c7 100644 --- a/app/code/Magento/User/Test/Mftf/Metadata/user_role-meta.xml +++ b/app/code/Magento/User/Test/Mftf/Metadata/user_role-meta.xml @@ -10,9 +10,10 @@ <operation name="CreateUserRole" dataType="user_role" type="create" auth="adminFormKey" url="/admin/user_role/saverole/" method="POST" successRegex="/messages-message-success/" returnRegex="" > <contentType>application/x-www-form-urlencoded</contentType> - <field key="rolename">string</field> - <field key="current_password">string</field> - <array key="resource"> + <field key="rolename" required="true">string</field> + <field key="current_password" required="true">string</field> + <field key="all" required="true">integer</field> + <array key="resource" required="false"> <value>string</value> </array> </operation> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml index 1b55d09d0597e..a3a82f6ce38e0 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml @@ -10,8 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminDeleteRoleSection"> <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> + <element name="salesRole" selector="//td[contains(text(), 'Sales')]" type="button"/> <element name="current_pass" type="button" selector="#current_password"/> <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> </section> </sections> + diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml new file mode 100644 index 0000000000000..21ca1cb36f988 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.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="AdminDeleteUserSection"> + <element name="role" parameterized="true" selector="//td[contains(text(), '{{roleName}}')]" type="button"/> + <element name="password" selector="#user_current_password" type="input"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> + <element name="confirm" selector=".action-primary.action-accept" type="button"/> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml index 9b030b216ce2c..8774261ea739d 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml @@ -19,6 +19,7 @@ <element name="password" type="input" selector="#page_tabs_main_section_content input[name='password']"/> <element name="passwordConfirmation" type="input" selector="#page_tabs_main_section_content input[name='password_confirmation']"/> <element name="interfaceLocale" type="select" selector="#page_tabs_main_section_content select[name='interface_locale']"/> + <element name="userIsActive" type="select" selector="#page_tabs_main_section_content select[id='user_is_active'] > option[value='{{isActive}}']" parameterized="true"/> <element name="currentPassword" type="input" selector="#page_tabs_main_section_content input[name='current_password']"/> <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminStoreSection.xml similarity index 57% rename from app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml rename to app/code/Magento/User/Test/Mftf/Section/AdminStoreSection.xml index 344330c4bc052..9e47351e68701 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminStoreSection.xml @@ -1,14 +1,13 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?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="ConfigurationMainActionsSection"> - <element name="save" type="button" selector="#save"/> + <section name="AdminStoreSection"> + <element name="createdRoleInUserPage" type="text" selector="//tr//td[contains(text(), '{{roleName}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml new file mode 100644 index 0000000000000..753ab02f84053 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml @@ -0,0 +1,49 @@ +<?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="AdminCreateActiveUserEntityTest"> + <annotations> + <features value="User"/> + <stories value="Create Admin User"/> + <title value="Admin user should be able to create active admin user"/> + <description value="Admin user should be able to create active admin user"/> + <testCaseId value=""/> + <severity value="CRITICAL"/> + <group value="user"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="activeAdmin"/> + </actionGroup> + </after> + + <actionGroup ref="AdminCreateUserWithRoleAndIsActiveActionGroup" stepKey="createAdminUser"> + <argument name="role" value="genericAdminRole"/> + <argument name="user" value="activeAdmin"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutMasterAdmin"/> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{activeAdmin.username}}" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{activeAdmin.password}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <closeAdminNotification stepKey="closeAdminNotification"/> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="navigateToAdminUsersGrid"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="{{activeAdmin.username}}" stepKey="fillUsernameSearch"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad time="10" stepKey="wait1"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="{{activeAdmin.username}}" stepKey="seeFoundUsername"/> + <actionGroup ref="logout" stepKey="logoutCreatedUser"/> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml new file mode 100644 index 0000000000000..d0fdd72ebbdcc --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml @@ -0,0 +1,48 @@ +<?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="AdminCreateInactiveUserEntityTest"> + <annotations> + <features value="User"/> + <stories value="Create Admin User"/> + <title value="Admin user should be able to create inactive admin user"/> + <description value="Admin user should be able to create inactive admin user"/> + <testCaseId value=""/> + <severity value="CRITICAL"/> + <group value="user"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="inactiveAdmin"/> + </actionGroup> + </after> + + <actionGroup ref="AdminCreateUserWithRoleAndIsActiveActionGroup" stepKey="createAdminUser"> + <argument name="role" value="genericAdminRole"/> + <argument name="user" value="inactiveAdmin"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="navigateToAdminUsersGrid"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="{{inactiveAdmin.username}}" stepKey="fillUsernameSearch"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad time="10" stepKey="wait1"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="{{inactiveAdmin.username}}" stepKey="seeFoundUsername"/> + <actionGroup ref="logout" stepKey="logoutMasterAdmin"/> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{inactiveAdmin.username}}" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{inactiveAdmin.password}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeUserErrorMessage" /> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml new file mode 100644 index 0000000000000..3bd55a454c3b0 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml @@ -0,0 +1,75 @@ +<?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="AdminLockAdminUserEntityTest"> + <annotations> + <features value="User"/> + <stories value="Lock admin user during login"/> + <title value="Lock admin user after entering incorrect password specified number of times"/> + <description value="Lock admin user after entering incorrect password specified number of times"/> + <testCaseId value="MC-14267"/> + <severity value="CRITICAL"/> + <group value="user"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI command="config:set admin/captcha/enable 0" stepKey="disableAdminCaptcha"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="LoginAsAdmin" stepKey="logIn"/> + </before> + <after> + <magentoCLI command="config:set admin/captcha/enable 1" stepKey="enableAdminCaptcha"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--Create New User--> + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="goToNewUserPage"/> + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="fillNewUserForm"> + <argument name="user" value="adminUserCorrectPassword"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="saveNewUser"/> + + <!--Set 'Maximum Login Failures to Lockout Account'--> + <actionGroup ref="AdminOpenConfigAdminPageActionGroup" stepKey="goToConfigAdminSectionPage"/> + <actionGroup ref="AdminExpandSecurityTabActionGroup" stepKey="openSecurityTab"/> + <actionGroup ref="AdminSetMaximumLoginFailuresToLockoutAccountActionGroup" stepKey="setMaximumLoginFailures"> + <argument name="qty" value="2"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveChanges"/> + + <!-- Log in to Admin Panel with incorrect password specified number of times--> + <actionGroup ref="logout" stepKey="logoutAsDefaultUser"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsNewUserFirstAttempt"> + <argument name="adminUser" value="adminUserIncorrectPassword"/> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="checkLoginErrorFirstAttempt"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsNewUserSecondAttempt"> + <argument name="adminUser" value="adminUserIncorrectPassword"/> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="checkLoginErrorSecondAttempt"/> + + <!-- Log in to Admin Panel with correct password--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsNewUserThirdAttempt"> + <argument name="adminUser" value="adminUserCorrectPassword"/> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="checkLoginErrorThirdAttempt"/> + + <!--Login as default admin user--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsDefaultAdminUser"/> + + <!--Delete new User--> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="adminUserCorrectPassword"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml new file mode 100644 index 0000000000000..dfadee8ee6807 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml @@ -0,0 +1,79 @@ +<?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="AdminUpdateUserTest"> + <annotations> + <features value="User"/> + <title value="Update admin user entity by changing user role"/> + <stories value="Update User" /> + <testCaseId value="MC-14264" /> + <severity value="MAJOR" /> + <description value="Change full access role for admin user to custom one with restricted permission (Sales)"/> + <group value="user"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="logIn"/> + + <!--Create New User--> + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="goToNewUserPage"/> + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="fillNewUserForm"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="saveNewUser"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="roleSales"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + </before> + <after> + <!--Delete new User--> + <actionGroup ref="logout" stepKey="logoutAsSaleRoleUser"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsDefaultAdminUser"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="AdminUserWithUpdatedUserRoleToSales"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRole"> + <argument name="roleName" value="{{roleSales.rolename}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + + <!--Assign new role--> + <actionGroup ref="AdminOpenUserEditPageActionGroup" stepKey="openUserEditPage"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="fillUserForm"> + <argument name="user" value="AdminUserWithUpdatedUserRoleToSales"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="saveUser"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="You saved the user."/> + </actionGroup> + + <actionGroup ref="AssertAdminUserIsInGridActionGroup" stepKey="seeUserInGrid"> + <argument name="user" value="AdminUserWithUpdatedUserRoleToSales"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsSaleRoleUser"> + <argument name="adminUser" value="AdminUserWithUpdatedUserRoleToSales"/> + </actionGroup> + <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="seeSuccessloginMessage"/> + <actionGroup ref="AdminOpenAdminUsersPageActionGroup" stepKey="navigateToAdminUsersPage"/> + <actionGroup ref="AssertUserRoleRestrictedAccessActionGroup" stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Unit/Model/Backend/Config/ObserverConfigTest.php b/app/code/Magento/User/Test/Unit/Model/Backend/Config/ObserverConfigTest.php new file mode 100644 index 0000000000000..395c45bc676a8 --- /dev/null +++ b/app/code/Magento/User/Test/Unit/Model/Backend/Config/ObserverConfigTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Test\Unit\Model\Backend\Config; + +use Magento\User\Model\Backend\Config\ObserverConfig; +use Magento\Backend\App\ConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Unit Test for \Magento\User\Model\Backend\Config\ObserverConfig class + * + * Class \Magento\User\Test\Unit\Model\Backend\Config\ObserverConfigTest + */ +class ObserverConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * Config path for lockout threshold + */ + private const XML_ADMIN_SECURITY_LOCKOUT_THRESHOLD = 'admin/security/lockout_threshold'; + + /** + * Config path for password change is forced or not + */ + private const XML_ADMIN_SECURITY_PASSWORD_IS_FORCED = 'admin/security/password_is_forced'; + + /** + * Config path for password lifetime + */ + private const XML_ADMIN_SECURITY_PASSWORD_LIFETIME = 'admin/security/password_lifetime'; + + /** + * Config path for maximum lockout failures + */ + private const XML_ADMIN_SECURITY_LOCKOUT_FAILURES = 'admin/security/lockout_failures'; + + /** @var ObserverConfig */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ConfigInterface + */ + private $backendConfigMock; + + /** + * Set environment for test + */ + protected function setUp() + { + $this->backendConfigMock = $this->createMock(ConfigInterface::class); + + $objectManager = new ObjectManagerHelper($this); + $this->model = $objectManager->getObject( + ObserverConfig::class, + [ + 'backendConfig' => $this->backendConfigMock + ] + ); + } + + /** + * Test when admin password lifetime = 0 days + */ + public function testIsLatestPasswordExpiredWhenNoAdminLifeTime() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_PASSWORD_LIFETIME) + ->willReturn('0'); + $this->assertEquals(false, $this->model->_isLatestPasswordExpired([])); + } + + /** + * Test when admin password lifetime = 2 days + */ + public function testIsLatestPasswordExpiredWhenHasAdminLifeTime() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_PASSWORD_LIFETIME) + ->willReturn('2'); + $this->assertEquals(true, $this->model->_isLatestPasswordExpired(['last_updated' => 1571428052])); + } + + /** + * Test when security lockout threshold = 100 minutes + */ + public function testGetAdminLockThreshold() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_LOCKOUT_THRESHOLD) + ->willReturn('100'); + $this->assertEquals(6000, $this->model->getAdminLockThreshold()); + } + + /** + * Test when password change force is true + */ + public function testIsPasswordChangeForcedTrue() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_PASSWORD_IS_FORCED) + ->willReturn('1'); + $this->assertEquals(true, $this->model->isPasswordChangeForced()); + } + + /** + * Test when password change force is false + */ + public function testIsPasswordChangeForcedFalse() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_PASSWORD_IS_FORCED) + ->willReturn('0'); + $this->assertEquals(false, $this->model->isPasswordChangeForced()); + } + + /** + * Test when admin password lifetime = 2 days + */ + public function testGetAdminPasswordLifetime() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_PASSWORD_LIFETIME) + ->willReturn('2'); + $this->assertEquals(172800, $this->model->getAdminPasswordLifetime()); + } + + /** + * Test when max failures = 5 (times) + */ + public function testGetMaxFailures() + { + $this->backendConfigMock->expects(self::any())->method('getValue') + ->with(self::XML_ADMIN_SECURITY_LOCKOUT_FAILURES) + ->willReturn('5'); + $this->assertEquals(5, $this->model->getMaxFailures()); + } +} diff --git a/app/code/Magento/User/etc/db_schema.xml b/app/code/Magento/User/etc/db_schema.xml index c3356a96b94a7..e175b50108bd9 100644 --- a/app/code/Magento/User/etc/db_schema.xml +++ b/app/code/Magento/User/etc/db_schema.xml @@ -46,9 +46,9 @@ </table> <table name="admin_passwords" resource="default" engine="innodb" comment="Admin Passwords"> <column xsi:type="int" name="password_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Password Id"/> + comment="Password ID"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="User Id"/> + comment="User ID"/> <column xsi:type="varchar" name="password_hash" nullable="true" length="100" comment="Password Hash"/> <column xsi:type="int" name="expires" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Deprecated"/> diff --git a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html index 891faf5fb8c2b..87f4e4669c4b6 100644 --- a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html +++ b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html @@ -6,7 +6,13 @@ --> <!--@subject {{trans "New admin user '%user_name' created" user_name=$user.name}} @--> <!--@vars { -"var store.getFrontendName()|escape":"Store Name" +"var store.frontend_name":"Store Name", +"var user.name":"User Name", +"var user.first_name":"User First Name", +"var user.last_name":"User Last Name", +"var user.email":"User Email", +"var store_email":"Store Email", +"var store_phone":"Store Phone" } @--> {{trans "Hello,"}} @@ -15,4 +21,4 @@ {{trans "If you have not authorized this action, please contact us immediately at %store_email" store_email=$store_email |escape}}{{depend store_phone}} {{trans "or call us at %store_phone" store_phone=$store_phone |escape}}{{/depend}}. {{trans "Thanks,"}} -{{var store.getFrontendName()}} +{{var store.frontend_name}} diff --git a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html index 62bac389e6dc4..dacfa640464a3 100644 --- a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html +++ b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html @@ -6,6 +6,9 @@ --> <!--@subject {{trans "Password Reset Confirmation for %name" name=$user.name}} @--> <!--@vars { +"var store.frontend_name":"Store Name", +"var user.id":"Account Holder Id", +"var user.rp_token":"Reset Password Token", "var user.name":"Account Holder Name", "store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL" } @--> @@ -21,4 +24,4 @@ {{trans "If you did not make this request, you can ignore this email and your password will remain the same."}} {{trans "Thank you,"}} -{{var store.getFrontendName()}} +{{var store.frontend_name}} diff --git a/app/code/Magento/User/view/adminhtml/email/user_notification.html b/app/code/Magento/User/view/adminhtml/email/user_notification.html index 3b6ffb2ce14b1..82657531a10df 100644 --- a/app/code/Magento/User/view/adminhtml/email/user_notification.html +++ b/app/code/Magento/User/view/adminhtml/email/user_notification.html @@ -6,13 +6,18 @@ --> <!--@subject {{trans "New %changes for %user_name" changes=$changes user_name=$user.name}} @--> <!--@vars { -"var store.getFrontendName()|escape":"Store Name" +"var store.frontend_name":"Store Name", +"var store_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var changes":"Changes", +"var user.name":"User Name" } @--> {{trans "Hello,"}} -{{trans "We have received a request to change the following information associated with your account at %store_name: %changes." store_name=$store.getFrontendName() changes=$changes}} +{{trans "We have received a request to change the following information associated with your account at %store_name: %changes." store_name=$store.frontend_name changes=$changes}} {{trans "If you have not authorized this action, please contact us immediately at %store_email" store_email=$store_email |escape}}{{depend store_phone}} {{trans "or call us at %store_phone" store_phone=$store_phone |escape}}{{/depend}}. {{trans "Thanks,"}} -{{var store.getFrontendName()}} +{{var store.frontend_name}} diff --git a/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml b/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml index edee167dc1b8a..97308204be854 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml @@ -38,13 +38,13 @@ <label class="label"><span><?= $block->escapeHtml(__('Resources')) ?></span></label> <div class="control"> - <div class="tree x-tree" data-role="resource-tree" data-mage-init='<?= /* @noEscape */ - $block->getJsonSerializer()->serialize([ + <div class="tree x-tree" data-role="resource-tree" data-mage-init='<?= + $block->escapeHtmlAttr($block->getJsonSerializer()->serialize([ 'rolesTree' => [ "treeInitData" => $block->getTree(), "treeInitSelectedData" => $block->getSelectedResources(), ], - ]); ?>'> + ])); ?>'> </div> </div> </div> diff --git a/app/code/Magento/Usps/Model/Carrier.php b/app/code/Magento/Usps/Model/Carrier.php index 7136a403003df..1c8ff0ce9efa9 100644 --- a/app/code/Magento/Usps/Model/Carrier.php +++ b/app/code/Magento/Usps/Model/Carrier.php @@ -8,7 +8,6 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Async\CallbackDeferred; -use Magento\Framework\Async\ProxyDeferredFactory; use Magento\Framework\HTTP\AsyncClient\Request; use Magento\Framework\HTTP\AsyncClientInterface; use Magento\Framework\Xml\Security; @@ -16,6 +15,7 @@ use Magento\Shipping\Helper\Carrier as CarrierHelper; use Magento\Shipping\Model\Carrier\AbstractCarrierOnline; use Magento\Shipping\Model\Rate\Result; +use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory; use Magento\Usps\Helper\Data as DataHelper; /** @@ -239,16 +239,17 @@ public function collectRates(RateRequest $request) //Saving current result to use the right one in the callback. $this->_result = $result = $this->_getQuotes(); - return $this->proxyDeferredFactory->createFor( - Result::class, - new CallbackDeferred( - function () use ($request, $result) { - $this->_result = $result; - $this->_updateFreeMethodQuote($request); + return $this->proxyDeferredFactory->create( + [ + 'deferred' => new CallbackDeferred( + function () use ($request, $result) { + $this->_result = $result; + $this->_updateFreeMethodQuote($request); - return $this->getResult(); - } - ) + return $this->getResult(); + } + ) + ] ); } @@ -555,18 +556,19 @@ protected function _getXmlQuotes() ) ); - return $this->proxyDeferredFactory->createFor( - Result::class, - new CallbackDeferred( - function () use ($deferredResponse, $request, $debugData) { - $responseBody = $deferredResponse->get()->getBody(); - $debugData['result'] = $responseBody; - $this->_setCachedQuotes($request, $responseBody); - $this->_debug($debugData); - - return $this->_parseXmlResponse($responseBody); - } - ) + return $this->proxyDeferredFactory->create( + [ + 'deferred' => new CallbackDeferred( + function () use ($deferredResponse, $request, $debugData) { + $responseBody = $deferredResponse->get()->getBody(); + $debugData['result'] = $responseBody; + $this->_setCachedQuotes($request, $responseBody); + $this->_debug($debugData); + + return $this->_parseXmlResponse($responseBody); + } + ) + ] ); } diff --git a/app/code/Magento/Usps/Test/Mftf/Section/AdminShippingMethodUSPSSection.xml b/app/code/Magento/Usps/Test/Mftf/Section/AdminShippingMethodUSPSSection.xml new file mode 100644 index 0000000000000..9226c40e13163 --- /dev/null +++ b/app/code/Magento/Usps/Test/Mftf/Section/AdminShippingMethodUSPSSection.xml @@ -0,0 +1,33 @@ +<?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="AdminShippingMethodUSPSSection"> + <element name="carriersUSPSTab" type="button" selector="#carriers_usps-head"/> + <element name="carriersUSPSActive" type="input" selector="#carriers_usps_active_inherit"/> + <element name="carriersUSPSGatewayXMLUrl" type="input" selector="#carriers_usps_gateway_url_inherit"/> + <element name="carriersUSPSGatewaySecureUrl" type="input" selector="#carriers_usps_gateway_secure_url_inherit"/> + <element name="carriersUSPSTitle" type="input" selector="#carriers_usps_title_inherit"/> + <element name="carriersUSPSUserId" type="input" selector="#carriers_usps_userid"/> + <element name="carriersUSPSPassword" type="input" selector="#carriers_usps_password"/> + <element name="carriersUSPSShipmentRequestType" type="select" selector="#carriers_usps_shipment_requesttype_inherit"/> + <element name="carriersUSPSContainer" type="input" selector="#carriers_usps_container_inherit"/> + <element name="carriersUSPSSize" type="input" selector="#carriers_usps_size_inherit"/> + <element name="carriersUSPSDestType" type="input" selector="#carriers_usps_machinable_inherit"/> + <element name="carriersUSPSMachinable" type="input" selector="#carriers_ups_tracking_xml_url_inherit"/> + <element name="carriersUSPSMaxPackageWeight" type="input" selector="#carriers_usps_max_package_weight_inherit"/> + <element name="carriersUSPSHandlingType" type="input" selector="#carriers_usps_handling_type_inherit"/> + <element name="carriersUSPSHandlingAction" type="input" selector="#carriers_usps_handling_action_inherit"/> + <element name="carriersUSPSAllowedMethods" type="input" selector="#carriers_usps_allowed_methods_inherit"/> + <element name="carriersUSPSFreeMethod" type="input" selector="#carriers_usps_free_method_inherit"/> + <element name="carriersUSPSSpecificErrMsg" type="input" selector="#carriers_usps_specificerrmsg_inherit"/> + <element name="carriersUSPSAllowSpecific" type="input" selector="#carriers_usps_sallowspecific_inherit"/> + <element name="carriersUSPSSpecificCountry" type="input" selector="#carriers_usps_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Usps/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Usps/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..cd77861fccd58 --- /dev/null +++ b/app/code/Magento/Usps/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,56 @@ +<?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="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in USPS section--> + <comment userInput="Assert configuration are disabled in USPS section" stepKey="commentSeeDisabledUSPSConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodUSPSSection.carriersUSPSTab}}" dependentSelector="{{AdminShippingMethodUSPSSection.carriersUSPSActive}}" visible="false" stepKey="expandUSPSTab"/> + <waitForElementVisible selector="{{AdminShippingMethodUSPSSection.carriersUSPSActive}}" stepKey="waitUSPSTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSActive}}" userInput="disabled" stepKey="grabUSPSActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSActiveDisabled" stepKey="assertUSPSActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSGatewayXMLUrl}}" userInput="disabled" stepKey="grabUSPSGatewayXMLUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSGatewayXMLUrlDisabled" stepKey="assertUSPSGatewayXMLUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSGatewaySecureUrl}}" userInput="disabled" stepKey="grabUSPSGatewaySecureUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSGatewaySecureUrlDisabled" stepKey="assertUSPSGatewaySecureUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSTitle}}" userInput="disabled" stepKey="grabUSPSTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSTitleDisabled" stepKey="assertUSPSTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSUserId}}" userInput="disabled" stepKey="grabUSPSUserIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSUserIdDisabled" stepKey="assertUSPSUserIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSPassword}}" userInput="disabled" stepKey="grabUSPSPasswordDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSPasswordDisabled" stepKey="assertUSPSPasswordDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSShipmentRequestType}}" userInput="disabled" stepKey="grabUSPSShipmentRequestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSShipmentRequestTypeDisabled" stepKey="assertUSPSShipmentRequestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSContainer}}" userInput="disabled" stepKey="grabUSPSContainerDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSContainerDisabled" stepKey="assertUSPSContainerDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSSize}}" userInput="disabled" stepKey="grabUSPSSizeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSSizeDisabled" stepKey="assertUSPSSizeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSDestType}}" userInput="disabled" stepKey="grabUSPSDestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSDestTypeDisabled" stepKey="assertUSPSDestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSMachinable}}" userInput="disabled" stepKey="grabUSPSMachinableDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSMachinableDisabled" stepKey="assertUSPSMachinableDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSMaxPackageWeight}}" userInput="disabled" stepKey="grabUSPSMaxPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSMaxPackageWeightDisabled" stepKey="assertUSPSMaxPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSHandlingType}}" userInput="disabled" stepKey="grabUSPSHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSHandlingTypeDisabled" stepKey="assertUSPSHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSHandlingAction}}" userInput="disabled" stepKey="grabUSPSHandlingActionDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSHandlingActionDisabled" stepKey="assertUSPSHandlingActionDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSAllowedMethods}}" userInput="disabled" stepKey="grabUSPSAllowedMethodsDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSAllowedMethodsDisabled" stepKey="assertUSPSAllowedMethodsDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSFreeMethod}}" userInput="disabled" stepKey="grabUSPSFreeMethodDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSFreeMethodDisabled" stepKey="assertUSPSFreeMethodDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSSpecificErrMsg}}" userInput="disabled" stepKey="grabUSPSSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSSpecificErrMsgDisabled" stepKey="assertUSPSSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSAllowSpecific}}" userInput="disabled" stepKey="grabUSPSAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSAllowSpecificDisabled" stepKey="assertUSPSAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSSpecificCountry}}" userInput="disabled" stepKey="grabUSPSSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSSpecificCountryDisabled" stepKey="assertUSPSSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Variable/Test/Mftf/ActionGroup/CreateCustomVariableActionGroup.xml b/app/code/Magento/Variable/Test/Mftf/ActionGroup/CreateCustomVariableActionGroup.xml index 1aa9a3fa6c6ab..75f09b478170e 100644 --- a/app/code/Magento/Variable/Test/Mftf/ActionGroup/CreateCustomVariableActionGroup.xml +++ b/app/code/Magento/Variable/Test/Mftf/ActionGroup/CreateCustomVariableActionGroup.xml @@ -21,20 +21,4 @@ <fillField selector="{{CustomVariableSection.variablePlain}}" userInput="{{customVariable.plain}}" stepKey="fillVariablePlain"/> <click selector="{{CustomVariableSection.saveCustomVariable}}" stepKey="clickSaveVariable"/> </actionGroup> - - <actionGroup name="DeleteCustomVariableActionGroup"> - <annotations> - <description>Goes to the Custom Variable grid page. Deletes the Custom Variable. PLEASE NOTE: The Custom Variable that is deleted is Hardcoded using 'customVariable'.</description> - </annotations> - - <amOnPage url="admin/admin/system_variable/" stepKey="goToVarialeGrid"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <click selector="{{CustomVariableSection.GridCustomVariableCode(customVariable.code)}}" stepKey="goToCustomVariableEditPage"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> - <waitForElementVisible selector="{{CustomVariableSection.delete}}" stepKey="waitForDeleteBtn"/> - <click selector="{{CustomVariableSection.delete}}" stepKey="deleteCustomVariable"/> - <waitForText userInput="Are you sure you want to do this?" stepKey="waitForText"/> - <click selector="{{CustomVariableSection.confirmDelete}}" stepKey="confirmDelete"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/Variable/Test/Mftf/ActionGroup/DeleteCustomVariableActionGroup.xml b/app/code/Magento/Variable/Test/Mftf/ActionGroup/DeleteCustomVariableActionGroup.xml new file mode 100644 index 0000000000000..df77d7d1fb9d0 --- /dev/null +++ b/app/code/Magento/Variable/Test/Mftf/ActionGroup/DeleteCustomVariableActionGroup.xml @@ -0,0 +1,26 @@ +<?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="DeleteCustomVariableActionGroup"> + <annotations> + <description>Goes to the Custom Variable grid page. Deletes the Custom Variable. PLEASE NOTE: The Custom Variable that is deleted is Hardcoded using 'customVariable'.</description> + </annotations> + + <amOnPage url="admin/admin/system_variable/" stepKey="goToVarialeGrid"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{CustomVariableSection.GridCustomVariableCode(customVariable.code)}}" stepKey="goToCustomVariableEditPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <waitForElementVisible selector="{{CustomVariableSection.delete}}" stepKey="waitForDeleteBtn"/> + <click selector="{{CustomVariableSection.delete}}" stepKey="deleteCustomVariable"/> + <waitForText userInput="Are you sure you want to do this?" stepKey="waitForText"/> + <click selector="{{CustomVariableSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Variable/Test/Unit/Model/Variable/DataTest.php b/app/code/Magento/Variable/Test/Unit/Model/Variable/DataTest.php new file mode 100644 index 0000000000000..50191de66efbf --- /dev/null +++ b/app/code/Magento/Variable/Test/Unit/Model/Variable/DataTest.php @@ -0,0 +1,128 @@ +<?php +/*** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Variable\Test\Unit\Model\Variable; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\TestCase; +use Magento\Variable\Model\Variable\Data as VariableDataModel; +use Magento\Variable\Model\ResourceModel\Variable\CollectionFactory as VariableCollectionFactory; +use Magento\Variable\Model\ResourceModel\Variable\Collection as VariableCollection; +use Magento\Variable\Model\Source\Variables as StoreVariables; + +class DataTest extends TestCase +{ + /** + * @var VariableDataModel + */ + private $model; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var StoreVariables|PHPUnit_Framework_MockObject_MockObject + */ + private $storesVariablesMock; + + /** + * @var VariableCollectionFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $variableCollectionFactoryMock; + + /** + * Set up before tests + */ + protected function setUp() + { + $this->storesVariablesMock = $this->createMock(StoreVariables::class); + $this->variableCollectionFactoryMock = $this->getMockBuilder( + VariableCollectionFactory::class + )->disableOriginalConstructor()->setMethods(['create'])->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + VariableDataModel::class, + [ + 'collectionFactory' => $this->variableCollectionFactoryMock, + 'storesVariables' => $this->storesVariablesMock + ] + ); + } + + /** + * Test getDefaultVariables() function + */ + public function testGetDefaultVariables() + { + $storesVariablesData = [ + [ + 'value' => 'test 1', + 'label' => 'Test Label 1', + 'group_label' => 'Group Label 1' + ], + [ + 'value' => 'test 2', + 'label' => 'Test Label 2', + 'group_label' => 'Group Label 2' + ] + ]; + $expectedResult = [ + [ + 'code' => 'test 1', + 'variable_name' => 'Group Label 1 / Test Label 1', + 'variable_type' => StoreVariables::DEFAULT_VARIABLE_TYPE + ], + [ + 'code' => 'test 2', + 'variable_name' => 'Group Label 2 / Test Label 2', + 'variable_type' => StoreVariables::DEFAULT_VARIABLE_TYPE + ] + ]; + $this->storesVariablesMock->expects($this->any())->method('getData')->willReturn($storesVariablesData); + + $this->assertEquals($expectedResult, $this->model->getDefaultVariables()); + } + + /** + * Test getCustomVariables() function + */ + public function testGetCustomVariables() + { + $customVariables = [ + [ + 'code' => 'test 1', + 'name' => 'Test 1' + ], + [ + 'code' => 'test 2', + 'name' => 'Test 2' + ] + ]; + $expectedResult = [ + [ + 'code' => 'test 1', + 'variable_name' => 'Custom Variable / Test 1', + 'variable_type' => StoreVariables::CUSTOM_VARIABLE_TYPE + ], + [ + 'code' => 'test 2', + 'variable_name' => 'Custom Variable / Test 2', + 'variable_type' => StoreVariables::CUSTOM_VARIABLE_TYPE + ] + ]; + $variableCollectionMock = $this->createMock(VariableCollection::class); + $this->variableCollectionFactoryMock->expects($this->once())->method('create') + ->willReturn($variableCollectionMock); + $variableCollectionMock->expects($this->any())->method('getData')->willReturn($customVariables); + + $this->assertEquals($expectedResult, $this->model->getCustomVariables()); + } +} diff --git a/app/code/Magento/Variable/etc/db_schema.xml b/app/code/Magento/Variable/etc/db_schema.xml index 239e3b49983c1..cd6d7d105a08a 100644 --- a/app/code/Magento/Variable/etc/db_schema.xml +++ b/app/code/Magento/Variable/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="variable" resource="default" engine="innodb" comment="Variables"> <column xsi:type="int" name="variable_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Variable Id"/> + comment="Variable ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Variable Code"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Variable Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -21,11 +21,11 @@ </table> <table name="variable_value" resource="default" engine="innodb" comment="Variable Value"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Variable Value Id"/> + comment="Variable Value ID"/> <column xsi:type="int" name="variable_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Variable Id"/> + default="0" comment="Variable ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="text" name="plain_value" nullable="true" comment="Plain Text Value"/> <column xsi:type="text" name="html_value" nullable="true" comment="Html Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Vault/Model/PaymentTokenSearchResults.php b/app/code/Magento/Vault/Model/PaymentTokenSearchResults.php new file mode 100644 index 0000000000000..39dcf503779b9 --- /dev/null +++ b/app/code/Magento/Vault/Model/PaymentTokenSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Vault\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Vault\Api\Data\PaymentTokenSearchResultsInterface; + +/** + * Service Data Object with Payment Token search results. + */ +class PaymentTokenSearchResults extends SearchResults implements PaymentTokenSearchResultsInterface +{ +} diff --git a/app/code/Magento/Vault/composer.json b/app/code/Magento/Vault/composer.json index 7dc2e0be78640..c37bc51f9d432 100644 --- a/app/code/Magento/Vault/composer.json +++ b/app/code/Magento/Vault/composer.json @@ -12,7 +12,8 @@ "magento/module-payment": "*", "magento/module-quote": "*", "magento/module-sales": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-theme": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Vault/etc/db_schema.xml b/app/code/Magento/Vault/etc/db_schema.xml index 8a7c8dc4aa9fb..7110978710048 100644 --- a/app/code/Magento/Vault/etc/db_schema.xml +++ b/app/code/Magento/Vault/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="varchar" name="public_hash" nullable="false" length="128" comment="Hash code for using on frontend"/> <column xsi:type="varchar" name="payment_method_code" nullable="false" length="128" @@ -42,9 +42,9 @@ <table name="vault_payment_token_order_payment_link" resource="default" engine="innodb" comment="Order payments to vault token"> <column xsi:type="int" name="order_payment_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order payment Id"/> + comment="Order payment ID"/> <column xsi:type="int" name="payment_token_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Payment token Id"/> + comment="Payment token ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="order_payment_id"/> <column name="payment_token_id"/> diff --git a/app/code/Magento/Vault/etc/di.xml b/app/code/Magento/Vault/etc/di.xml index 95191e4417576..0192a783bd5a8 100644 --- a/app/code/Magento/Vault/etc/di.xml +++ b/app/code/Magento/Vault/etc/di.xml @@ -13,7 +13,7 @@ <preference for="Magento\Vault\Api\PaymentTokenRepositoryInterface" type="Magento\Vault\Model\PaymentTokenRepository" /> <preference for="Magento\Vault\Api\PaymentTokenManagementInterface" type="Magento\Vault\Model\PaymentTokenManagement" /> <preference for="Magento\Vault\Api\PaymentMethodListInterface" type="Magento\Vault\Model\PaymentMethodList" /> - <preference for="Magento\Vault\Api\Data\PaymentTokenSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Vault\Api\Data\PaymentTokenSearchResultsInterface" type="Magento\Vault\Model\PaymentTokenSearchResults" /> <preference for="Magento\Vault\Model\Ui\TokenUiComponentInterface" type="Magento\Vault\Model\Ui\TokenUiComponent" /> <type name="Magento\Sales\Api\Data\OrderPaymentInterface"> diff --git a/app/code/Magento/Vault/view/frontend/layout/customer_account.xml b/app/code/Magento/Vault/view/frontend/layout/customer_account.xml index 73ce9247fef0a..05044da272e6d 100644 --- a/app/code/Magento/Vault/view/frontend/layout/customer_account.xml +++ b/app/code/Magento/Vault/view/frontend/layout/customer_account.xml @@ -11,7 +11,6 @@ <referenceBlock name="customer_account_navigation"> <block class="Magento\Customer\Block\Account\SortLinkInterface" name="customer-account-navigation-my-credit-cards-link" - ifconfig="payment/braintree/active" > <arguments> <argument name="path" xsi:type="string">vault/cards/listaction</argument> diff --git a/app/code/Magento/Version/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Version/Test/Unit/Controller/Index/IndexTest.php new file mode 100644 index 0000000000000..6f8daa9d8008d --- /dev/null +++ b/app/code/Magento/Version/Test/Unit/Controller/Index/IndexTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Version\Test\Unit\Controller\Index; + +use Magento\Version\Controller\Index\Index as VersionIndex; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ProductMetadataInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class \Magento\Version\Test\Unit\Controller\Index\IndexTest + */ +class IndexTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var VersionIndex + */ + private $model; + + /** + * @var Context + */ + private $context; + + /** + * @var ProductMetadataInterface + */ + private $productMetadata; + + /** + * @var ResponseInterface + */ + private $response; + + /** + * Prepare test preconditions + */ + protected function setUp() + { + $this->context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->productMetadata = $this->getMockBuilder(ProductMetadataInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'getEdition', 'getVersion']) + ->getMock(); + + $this->response = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setBody', 'sendResponse']) + ->getMock(); + + $this->context->expects($this->any()) + ->method('getResponse') + ->willReturn($this->response); + + $helper = new ObjectManager($this); + + $this->model = $helper->getObject( + 'Magento\Version\Controller\Index\Index', + [ + 'context' => $this->context, + 'productMetadata' => $this->productMetadata + ] + ); + } + + /** + * Test with Git Base version + */ + public function testExecuteWithGitBase() + { + $this->productMetadata->expects($this->any())->method('getVersion')->willReturn('dev-2.3'); + $this->assertNull($this->model->execute()); + } + + /** + * Test with Community Version + */ + public function testExecuteWithCommunityVersion() + { + $this->productMetadata->expects($this->any())->method('getVersion')->willReturn('2.3.3'); + $this->productMetadata->expects($this->any())->method('getEdition')->willReturn('Community'); + $this->productMetadata->expects($this->any())->method('getName')->willReturn('Magento'); + $this->response->expects($this->once())->method('setBody') + ->with('Magento/2.3 (Community)') + ->will($this->returnSelf()); + $this->model->execute(); + } +} diff --git a/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php b/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php index d89513b50c9c5..8dcaabda93aab 100644 --- a/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php +++ b/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php @@ -99,7 +99,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getUserId() { @@ -108,7 +108,7 @@ public function getUserId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getUserType() { @@ -187,6 +187,8 @@ protected function processRequest() } /** + * Set user data based on user type received from token data. + * * @param Token $token * @return void */ diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index 3ddb2e441ef91..f38c0f0978536 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -33,9 +33,7 @@ class Generator extends AbstractSchemaGenerator */ const ERROR_SCHEMA = '#/definitions/error-response'; - /** - * Unauthorized description - */ + /** Unauthorized description */ const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized'; /** Array signifier */ @@ -759,7 +757,8 @@ private function handleComplex($name, $type, $prefix, $isArray) $subPrefix ); } - return array_merge(...$queryNames); + + return empty($queryNames) ? [] : array_merge(...$queryNames); } /** diff --git a/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php b/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php index 172db875c6c49..67e361bb019d0 100644 --- a/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php +++ b/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Webapi\Test\Unit\Model\Rest\Swagger; /** @@ -137,11 +138,7 @@ public function testGenerate($serviceMetadata, $typeData, $schema) ->willReturn($serviceMetadata); $this->typeProcessorMock->expects($this->any()) ->method('getTypeData') - ->willReturnMap( - [ - ['TestModule5V2EntityAllSoapAndRest', $typeData], - ] - ); + ->willReturnMap($typeData); $this->typeProcessorMock->expects($this->any()) ->method('isTypeSimple') @@ -169,6 +166,96 @@ public function testGenerate($serviceMetadata, $typeData, $schema) public function generateDataProvider() { return [ + [ + [ + 'methods' => [ + 'execute' => [ + 'method' => 'execute', + 'inputRequired' => false, + 'isSecure' => false, + 'resources' => [ + "anonymous" + ], + 'methodAlias' => 'execute', + 'parameters' => [], + 'documentation' => 'Do Magic!', + 'interface' => [ + 'in' => [ + 'parameters' => [ + 'searchRequest' => [ + 'type' => 'DreamVendorDreamModuleApiDataSearchRequestInterface', + 'required' => true, + 'documentation' => "" + ] + ] + ], + 'out' => [ + 'parameters' => [ + 'result' => [ + 'type' => 'DreamVendorDreamModuleApiDataSearchResultInterface', + 'documentation' => null, + 'required' => true + ] + ] + ] + ] + ] + ], + 'class' => 'DreamVendor\DreamModule\Api\ExecuteStuff', + 'description' => '', + 'routes' => [ + '/V1/dream-vendor/dream-module/execute-stuff' => [ + 'GET' => [ + 'method' => 'execute', + 'parameters' => [] + ] + ] + ] + ], + [ + [ + 'DreamVendorDreamModuleApiDataSearchRequestInterface', + [ + 'documentation' => '', + 'parameters' => [ + 'stuff' => [ + 'type' => 'DreamVendorDreamModuleApiDataStuffInterface', + 'required' => true, + 'documentation' => 'Empty Extension Point' + ] + ] + ] + ], + [ + 'DreamVendorDreamModuleApiDataSearchResultInterface', + [ + 'documentation' => '', + 'parameters' => [ + 'totalCount' => [ + 'type' => 'int', + 'required' => true, + 'documentation' => 'Processed count.' + ], + 'stuff' => [ + 'type' => 'DreamVendorDreamModuleApiDataStuffInterface', + 'required' => true, + 'documentation' => 'Empty Extension Point' + ] + ] + ] + ], + [ + 'DreamVendorDreamModuleApiDataStuffInterface', + [ + 'documentation' => '', + 'parameters' => [] + ] + ] + ], + // @codingStandardsIgnoreStart + '{"swagger":"2.0","info":{"version":"","title":""},"host":"magento.host","basePath":"/rest/default","schemes":["http://"],"tags":[{"name":"testModule5AllSoapAndRestV2","description":""}],"paths":{"/V1/dream-vendor/dream-module/execute-stuff":{"get":{"tags":["testModule5AllSoapAndRestV2"],"description":"Do Magic!","operationId":"operationNameGet","consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"responses":{"200":{"description":"200 Success.","schema":{"$ref":"#/definitions/dream-vendor-dream-module-api-data-search-result-interface"}},"default":{"description":"Unexpected error","schema":{"$ref":"#/definitions/error-response"}}}}}},"definitions":{"error-response":{"type":"object","properties":{"message":{"type":"string","description":"Error message"},"errors":{"$ref":"#/definitions/error-errors"},"code":{"type":"integer","description":"Error code"},"parameters":{"$ref":"#/definitions/error-parameters"},"trace":{"type":"string","description":"Stack trace"}},"required":["message"]},"error-errors":{"type":"array","description":"Errors list","items":{"$ref":"#/definitions/error-errors-item"}},"error-errors-item":{"type":"object","description":"Error details","properties":{"message":{"type":"string","description":"Error message"},"parameters":{"$ref":"#/definitions/error-parameters"}}},"error-parameters":{"type":"array","description":"Error parameters list","items":{"$ref":"#/definitions/error-parameters-item"}},"error-parameters-item":{"type":"object","description":"Error parameters item","properties":{"resources":{"type":"string","description":"ACL resource"},"fieldName":{"type":"string","description":"Missing or invalid field name"},"fieldValue":{"type":"string","description":"Incorrect field value"}}},"dream-vendor-dream-module-api-data-search-result-interface":{"type":"object","description":"","properties":{"total_count":{"type":"integer","description":"Processed count."},"stuff":{"$ref":"#/definitions/dream-vendor-dream-module-api-data-stuff-interface"}},"required":["total_count","stuff"]},"dream-vendor-dream-module-api-data-stuff-interface":{"type":"object","description":""}}}' + // @codingStandardsIgnoreEnd + ], [ [ 'methods' => [ @@ -213,12 +300,17 @@ public function generateDataProvider() ], ], [ - 'documentation' => 'Some Data Object', - 'parameters' => [ - 'price' => [ - 'type' => 'int', - 'required' => true, - 'documentation' => "" + [ + 'TestModule5V2EntityAllSoapAndRest', + [ + 'documentation' => 'Some Data Object', + 'parameters' => [ + 'price' => [ + 'type' => 'int', + 'required' => true, + 'documentation' => "" + ] + ] ] ] ], @@ -261,12 +353,17 @@ public function generateDataProvider() ], ], [ - 'documentation' => 'Some Data Object', - 'parameters' => [ - 'price' => [ - 'type' => 'int', - 'required' => true, - 'documentation' => "" + [ + 'TestModule5V2EntityAllSoapAndRest', + [ + 'documentation' => 'Some Data Object', + 'parameters' => [ + 'price' => [ + 'type' => 'int', + 'required' => true, + 'documentation' => "" + ] + ] ] ] ], diff --git a/app/code/Magento/Weee/Block/Sales/Order/Totals.php b/app/code/Magento/Weee/Block/Sales/Order/Totals.php index 8aeefecb14cc9..bc04b3a3985f3 100644 --- a/app/code/Magento/Weee/Block/Sales/Order/Totals.php +++ b/app/code/Magento/Weee/Block/Sales/Order/Totals.php @@ -6,6 +6,8 @@ namespace Magento\Weee\Block\Sales\Order; /** + * Wee tax total column block + * * @api * @since 100.0.2 */ @@ -54,6 +56,8 @@ public function initTotals() $weeeTotal = $this->weeeData->getTotalAmounts($items, $store); $weeeBaseTotal = $this->weeeData->getBaseTotalAmounts($items, $store); if ($weeeTotal) { + $totals = $this->getParentBlock()->getTotals(); + // Add our total information to the set of other totals $total = new \Magento\Framework\DataObject( [ @@ -63,10 +67,10 @@ public function initTotals() 'base_value' => $weeeBaseTotal ] ); - if ($this->getBeforeCondition()) { - $this->getParentBlock()->addTotalBefore($total, $this->getBeforeCondition()); + if (isset($totals['grand_total_incl'])) { + $this->getParentBlock()->addTotalBefore($total, 'grand_total'); } else { - $this->getParentBlock()->addTotal($total, $this->getAfterCondition()); + $this->getParentBlock()->addTotalBefore($total, $this->getBeforeCondition()); } } return $this; diff --git a/app/code/Magento/Weee/Model/App/Action/ContextPlugin.php b/app/code/Magento/Weee/Model/App/Action/ContextPlugin.php index 5d5426660d8f1..aae6f769eb500 100644 --- a/app/code/Magento/Weee/Model/App/Action/ContextPlugin.php +++ b/app/code/Magento/Weee/Model/App/Action/ContextPlugin.php @@ -33,7 +33,7 @@ class ContextPlugin protected $weeeHelper; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManager; @@ -63,7 +63,7 @@ class ContextPlugin * @param \Magento\Weee\Model\Tax $weeeTax * @param \Magento\Tax\Helper\Data $taxHelper * @param \Magento\Weee\Helper\Data $weeeHelper - * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager + * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\PageCache\Model\Config $cacheConfig * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -74,7 +74,7 @@ public function __construct( \Magento\Weee\Model\Tax $weeeTax, \Magento\Tax\Helper\Data $taxHelper, \Magento\Weee\Helper\Data $weeeHelper, - \Magento\Framework\Module\ModuleManagerInterface $moduleManager, + \Magento\Framework\Module\Manager $moduleManager, \Magento\PageCache\Model\Config $cacheConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig diff --git a/app/code/Magento/Weee/Model/ResourceModel/Tax.php b/app/code/Magento/Weee/Model/ResourceModel/Tax.php index b097e4a018f22..2cbb6054a31ed 100644 --- a/app/code/Magento/Weee/Model/ResourceModel/Tax.php +++ b/app/code/Magento/Weee/Model/ResourceModel/Tax.php @@ -5,9 +5,6 @@ */ namespace Magento\Weee\Model\ResourceModel; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Condition\ConditionInterface; - /** * Wee tax resource model * @@ -21,6 +18,11 @@ class Tax extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $dateTime; + /** + * @var array + */ + private $weeeTaxCalculationsByEntityCache = []; + /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\Stdlib\DateTime $dateTime @@ -46,7 +48,7 @@ protected function _construct() } /** - * Fetch one + * Fetch one calculated weee attribute from a select criteria * * @param \Magento\Framework\DB\Select|string $select * @return string @@ -57,6 +59,8 @@ public function fetchOne($select) } /** + * Is there a weee attribute available for the location provided + * * @param int $countryId * @param int $regionId * @param int $websiteId @@ -91,6 +95,8 @@ public function isWeeeInLocation($countryId, $regionId, $websiteId) } /** + * Fetch calculated weee attributes by location, store and entity + * * @param int $countryId * @param int $regionId * @param int $websiteId @@ -100,43 +106,56 @@ public function isWeeeInLocation($countryId, $regionId, $websiteId) */ public function fetchWeeeTaxCalculationsByEntity($countryId, $regionId, $websiteId, $storeId, $entityId) { - $attributeSelect = $this->getConnection()->select(); - $attributeSelect->from( - ['eavTable' => $this->getTable('eav_attribute')], - ['eavTable.attribute_code', 'eavTable.attribute_id', 'eavTable.frontend_label'] - )->joinLeft( - ['eavLabel' => $this->getTable('eav_attribute_label')], - 'eavLabel.attribute_id = eavTable.attribute_id and eavLabel.store_id = ' .((int) $storeId), - 'eavLabel.value as label_value' - )->joinInner( - ['weeeTax' => $this->getTable('weee_tax')], - 'weeeTax.attribute_id = eavTable.attribute_id', - 'weeeTax.value as weee_value' - )->where( - 'eavTable.frontend_input = ?', - 'weee' - )->where( - 'weeeTax.website_id IN(?)', - [$websiteId, 0] - )->where( - 'weeeTax.country = ?', - $countryId - )->where( - 'weeeTax.state IN(?)', - [$regionId, 0] - )->where( - 'weeeTax.entity_id = ?', - (int)$entityId + $cacheKey = sprintf( + '%s-%s-%s-%s-%s', + $countryId, + $regionId, + $websiteId, + $storeId, + $entityId ); + if (!isset($this->weeeTaxCalculationsByEntityCache[$cacheKey])) { + $attributeSelect = $this->getConnection()->select(); + $attributeSelect->from( + ['eavTable' => $this->getTable('eav_attribute')], + ['eavTable.attribute_code', 'eavTable.attribute_id', 'eavTable.frontend_label'] + )->joinLeft( + ['eavLabel' => $this->getTable('eav_attribute_label')], + 'eavLabel.attribute_id = eavTable.attribute_id and eavLabel.store_id = ' . ((int)$storeId), + 'eavLabel.value as label_value' + )->joinInner( + ['weeeTax' => $this->getTable('weee_tax')], + 'weeeTax.attribute_id = eavTable.attribute_id', + 'weeeTax.value as weee_value' + )->where( + 'eavTable.frontend_input = ?', + 'weee' + )->where( + 'weeeTax.website_id IN(?)', + [$websiteId, 0] + )->where( + 'weeeTax.country = ?', + $countryId + )->where( + 'weeeTax.state IN(?)', + [$regionId, 0] + )->where( + 'weeeTax.entity_id = ?', + (int)$entityId + ); - $order = ['weeeTax.state ' . \Magento\Framework\DB\Select::SQL_DESC, - 'weeeTax.website_id ' . \Magento\Framework\DB\Select::SQL_DESC]; - $attributeSelect->order($order); + $order = ['weeeTax.state ' . \Magento\Framework\DB\Select::SQL_DESC, + 'weeeTax.website_id ' . \Magento\Framework\DB\Select::SQL_DESC]; + $attributeSelect->order($order); - $values = $this->getConnection()->fetchAll($attributeSelect); + $values = $this->getConnection()->fetchAll($attributeSelect); - if ($values) { - return $values; + if ($values) { + $this->weeeTaxCalculationsByEntityCache[$cacheKey] = $values; + return $values; + } + } else { + return $this->weeeTaxCalculationsByEntityCache[$cacheKey]; } return []; diff --git a/app/code/Magento/Weee/Observer/AfterAddressSave.php b/app/code/Magento/Weee/Observer/AfterAddressSave.php index 9acea506adf67..ba15854b2dff4 100644 --- a/app/code/Magento/Weee/Observer/AfterAddressSave.php +++ b/app/code/Magento/Weee/Observer/AfterAddressSave.php @@ -8,7 +8,7 @@ use Magento\Customer\Model\Address; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\PageCache\Model\Config; use Magento\Tax\Api\TaxAddressManagerInterface; use Magento\Weee\Helper\Data; @@ -26,7 +26,7 @@ class AfterAddressSave implements ObserverInterface /** * Module manager * - * @var ModuleManagerInterface + * @var Manager */ private $moduleManager; @@ -46,13 +46,13 @@ class AfterAddressSave implements ObserverInterface /** * @param Data $weeeHelper - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param Config $cacheConfig * @param TaxAddressManagerInterface $addressManager */ public function __construct( Data $weeeHelper, - ModuleManagerInterface $moduleManager, + Manager $moduleManager, Config $cacheConfig, TaxAddressManagerInterface $addressManager ) { diff --git a/app/code/Magento/Weee/Observer/CustomerLoggedIn.php b/app/code/Magento/Weee/Observer/CustomerLoggedIn.php index 95299d96cabd2..0b22c24d7fa25 100644 --- a/app/code/Magento/Weee/Observer/CustomerLoggedIn.php +++ b/app/code/Magento/Weee/Observer/CustomerLoggedIn.php @@ -8,7 +8,7 @@ use Magento\Customer\Model\Session; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; -use Magento\Framework\Module\ModuleManagerInterface; +use Magento\Framework\Module\Manager; use Magento\PageCache\Model\Config; use Magento\Tax\Api\TaxAddressManagerInterface; use Magento\Weee\Helper\Data; @@ -52,13 +52,13 @@ class CustomerLoggedIn implements ObserverInterface /** * @param Data $weeeHelper - * @param ModuleManagerInterface $moduleManager + * @param Manager $moduleManager * @param Config $cacheConfig * @param TaxAddressManagerInterface $addressManager */ public function __construct( Data $weeeHelper, - ModuleManagerInterface $moduleManager, + Manager $moduleManager, Config $cacheConfig, TaxAddressManagerInterface $addressManager ) { diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml index 85ed044644d5c..aeb8537313fae 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -11,6 +11,7 @@ <test name="AdminFixedTaxValSavedForSpecificWebsiteTest"> <annotations> <features value="Tax"/> + <stories value="Website Specific Fixed Product Tax"/> <title value="Fixed Product Tax value is saved correctly for Specific Website"/> <description value="Fixed Product Tax value is saved correctly for Specific Website"/> <severity value="MAJOR"/> diff --git a/app/code/Magento/Weee/Test/Unit/App/Action/ContextPluginTest.php b/app/code/Magento/Weee/Test/Unit/App/Action/ContextPluginTest.php index b720f42378fa9..c829b524527a6 100644 --- a/app/code/Magento/Weee/Test/Unit/App/Action/ContextPluginTest.php +++ b/app/code/Magento/Weee/Test/Unit/App/Action/ContextPluginTest.php @@ -39,7 +39,7 @@ class ContextPluginTest extends \PHPUnit\Framework\TestCase protected $taxCalculationMock; /** - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ protected $moduleManagerMock; @@ -93,7 +93,7 @@ protected function setUp() ) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Weee/Test/Unit/Observer/AfterAddressSaveTest.php b/app/code/Magento/Weee/Test/Unit/Observer/AfterAddressSaveTest.php index a7b88f5727126..868d603f34b8c 100644 --- a/app/code/Magento/Weee/Test/Unit/Observer/AfterAddressSaveTest.php +++ b/app/code/Magento/Weee/Test/Unit/Observer/AfterAddressSaveTest.php @@ -18,7 +18,7 @@ class AfterAddressSaveTest extends \PHPUnit\Framework\TestCase * @var ObjectManager */ private $objectManager; - + /** * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject */ @@ -27,7 +27,7 @@ class AfterAddressSaveTest extends \PHPUnit\Framework\TestCase /** * Module manager * - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ private $moduleManagerMock; @@ -42,7 +42,7 @@ class AfterAddressSaveTest extends \PHPUnit\Framework\TestCase * @var \Magento\Weee\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ private $weeeHelperMock; - + /** * @var TaxAddressManagerInterface|MockObject */ @@ -61,7 +61,7 @@ protected function setUp() ->setMethods(['getCustomerAddress']) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); @@ -129,7 +129,7 @@ public function testExecute( $this->addressManagerMock->expects($isNeedSetAddress ? $this->once() : $this->never()) ->method('setDefaultAddressAfterSave') ->with($address); - + $this->session->execute($this->observerMock); } diff --git a/app/code/Magento/Weee/Test/Unit/Observer/CustomerLoggedInTest.php b/app/code/Magento/Weee/Test/Unit/Observer/CustomerLoggedInTest.php index 06d1dbedcfd80..af8c2e70a8ff6 100644 --- a/app/code/Magento/Weee/Test/Unit/Observer/CustomerLoggedInTest.php +++ b/app/code/Magento/Weee/Test/Unit/Observer/CustomerLoggedInTest.php @@ -21,7 +21,7 @@ class CustomerLoggedInTest extends \PHPUnit\Framework\TestCase /** * Module manager * - * @var \Magento\Framework\Module\ModuleManagerInterface + * @var \Magento\Framework\Module\Manager */ private $moduleManagerMock; @@ -59,7 +59,7 @@ protected function setUp() ) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Weee/etc/db_schema.xml b/app/code/Magento/Weee/etc/db_schema.xml index 1b07168247011..aed8318993acf 100644 --- a/app/code/Magento/Weee/etc/db_schema.xml +++ b/app/code/Magento/Weee/etc/db_schema.xml @@ -9,9 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="weee_tax" resource="default" engine="innodb" comment="Weee Tax"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="varchar" name="country" nullable="true" length="2" comment="Country"/> @@ -20,7 +20,7 @@ <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="State"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/FixedProductTax.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/FixedProductTax.php new file mode 100644 index 0000000000000..98164c18e858f --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/FixedProductTax.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Weee\Helper\Data; +use Magento\Framework\Exception\LocalizedException; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Tax\Model\Config; + +/** + * Resolver for FixedProductTax object that retrieves an array of FPT attributes with prices + */ +class FixedProductTax implements ResolverInterface +{ + /** + * @var Data + */ + private $weeeHelper; + + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @param Data $weeeHelper + * @param TaxHelper $taxHelper + */ + public function __construct(Data $weeeHelper, TaxHelper $taxHelper) + { + $this->weeeHelper = $weeeHelper; + $this->taxHelper = $taxHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $fptArray = []; + $product = $value['model']; + + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + if ($this->weeeHelper->isEnabled($store)) { + $attributes = $this->weeeHelper->getProductWeeeAttributesForDisplay($product); + foreach ($attributes as $attribute) { + $displayInclTaxes = $this->taxHelper->getPriceDisplayType($store); + $amount = $attribute->getData('amount'); + //add display mode for WEE to not return WEE if excluded + if ($displayInclTaxes === Config::DISPLAY_TYPE_EXCLUDING_TAX) { + $amount = $attribute->getData('amount_excl_tax'); + } elseif ($displayInclTaxes === Config::DISPLAY_TYPE_INCLUDING_TAX) { + $amount = $attribute->getData('amount_excl_tax') + $attribute->getData('tax_amount'); + } + $fptArray[] = [ + 'amount' => [ + 'value' => $amount, + 'currency' => $value['final_price']['currency'], + ], + 'label' => $attribute->getData('name') + ]; + } + } + + return $fptArray; + } +} diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php new file mode 100644 index 0000000000000..d2ea44fff5bcc --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Weee\Helper\Data; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Weee\Model\Tax as WeeeDisplayConfig; +use Magento\Framework\Pricing\Render; + +/** + * Resolver for the FPT store config settings + */ +class StoreConfig implements ResolverInterface +{ + /** + * @var string + */ + private static $weeeDisplaySettingsNone = 'FPT_DISABLED'; + + /** + * @var array + */ + private static $weeeDisplaySettings = [ + WeeeDisplayConfig::DISPLAY_INCL => 'INCLUDE_FPT_WITHOUT_DETAILS', + WeeeDisplayConfig::DISPLAY_INCL_DESCR => 'INCLUDE_FPT_WITH_DETAILS', + WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL => 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + WeeeDisplayConfig::DISPLAY_EXCL => 'EXCLUDE_FPT_WITHOUT_DETAILS' + ]; + + /** + * @var Data + */ + private $weeeHelper; + + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var array + */ + private $computedFptSettings = []; + + /** + * @param Data $weeeHelper + * @param TaxHelper $taxHelper + */ + public function __construct(Data $weeeHelper, TaxHelper $taxHelper) + { + $this->weeeHelper = $weeeHelper; + $this->taxHelper = $taxHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($this->computedFptSettings)) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + + $this->computedFptSettings = [ + 'product_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + 'category_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + 'sales_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + ]; + if ($this->weeeHelper->isEnabled($store)) { + $productFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_ITEM_VIEW, $storeId); + $categoryFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_ITEM_LIST, $storeId); + $salesModulesFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_SALES, $storeId); + + $this->computedFptSettings = [ + 'product_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$productFptDisplay] ?? + self::$weeeDisplaySettingsNone, + 'category_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$categoryFptDisplay] ?? + self::$weeeDisplaySettingsNone, + 'sales_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$salesModulesFptDisplay] ?? + self::$weeeDisplaySettingsNone, + ]; + } + } + + return $this->computedFptSettings[$info->fieldName] ?? null; + } + + /** + * Get the weee system display setting + * + * @param string $zone + * @param string $storeId + * @return string + */ + private function getWeeDisplaySettingsByZone(string $zone, int $storeId): int + { + return (int) $this->weeeHelper->typeOfDisplay( + null, + $zone, + $storeId + ); + } +} diff --git a/app/code/Magento/WeeeGraphQl/composer.json b/app/code/Magento/WeeeGraphQl/composer.json index 0bf303f789a7f..39b77bb569ac6 100644 --- a/app/code/Magento/WeeeGraphQl/composer.json +++ b/app/code/Magento/WeeeGraphQl/composer.json @@ -4,10 +4,12 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0||~7.3.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-weee": "*" }, "suggest": { - "magento/module-weee": "*", "magento/module-catalog-graph-ql": "*" }, "license": [ diff --git a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls index 731260ce9e1e0..18b0e7c1823e8 100644 --- a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls @@ -2,6 +2,29 @@ # See COPYING.txt for license details. enum PriceAdjustmentCodesEnum { - WEE - WEETAX + WEEE @deprecated(reason: "WEEE code is deprecated, use fixed_product_taxes.label") + WEEE_TAX @deprecated(reason: "Use fixed_product_taxes. PriceAdjustmentCodesEnum is deprecated. Tax is included or excluded in price. Tax is not shown separtely in Catalog") +} + +type ProductPrice { + fixed_product_taxes: [FixedProductTax] @doc(description: "The multiple FPTs that can be applied to a product price.") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\FixedProductTax") +} + +type FixedProductTax @doc(description: "A single FPT that can be applied to a product price.") { + amount: Money @doc(description: "Amount of the FPT as a money object.") + label: String @doc(description: "The label assigned to the FPT to be displayed on the frontend.") +} + +type StoreConfig { + product_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices On Product View Page' field. It indicates how FPT information is displayed on product pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") + category_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices In Product Lists' field. It indicates how FPT information is displayed on category pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") + sales_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices In Sales Modules' field. It indicates how FPT information is displayed on cart, checkout, and order pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") +} + +enum FixedProductTaxDisplaySettings @doc(description: "This enumeration display settings for the fixed product tax") { + INCLUDE_FPT_WITHOUT_DETAILS @doc(description: "The displayed price includes the FPT amount without displaying the ProductPrice.fixed_product_taxes values. This value corresponds to 'Including FPT only'") + INCLUDE_FPT_WITH_DETAILS @doc(description: "The displayed price includes the FPT amount while displaying the values of ProductPrice.fixed_product_taxes separately. This value corresponds to 'Including FPT and FPT description'") + EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS @doc(description: "The displayed price does not include the FPT amount. The values of ProductPrice.fixed_product_taxes and the price including the FPT are displayed separately. This value corresponds to 'Excluding FPT, Including FPT description and final price'") + EXCLUDE_FPT_WITHOUT_DETAILS @doc(description: "The displayed price does not include the FPT amount. The values from ProductPrice.fixed_product_taxes are not displayed. This value corresponds to 'Excluding FPT'") + FPT_DISABLED @doc(description: "The FPT feature is not enabled. You can omit ProductPrice.fixed_product_taxes from your query") } diff --git a/app/code/Magento/Widget/Model/Template/FilterEmulate.php b/app/code/Magento/Widget/Model/Template/FilterEmulate.php index 9e3b571587a50..4312003c7f34e 100644 --- a/app/code/Magento/Widget/Model/Template/FilterEmulate.php +++ b/app/code/Magento/Widget/Model/Template/FilterEmulate.php @@ -3,18 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Widget\Model\Template; +/** + * Class FilterEmulate + * + * @package Magento\Widget\Model\Template + */ class FilterEmulate extends Filter { /** * Generate widget with emulation frontend area * * @param string[] $construction - * @return string + * + * @return mixed|string + * @throws \Exception */ public function widgetDirective($construction) { return $this->_appState->emulateAreaCode('frontend', [$this, 'generateWidget'], [$construction]); } + + /** + * Filter the string as template with frontend area emulation + * + * @param string $value + * + * @return string + * @throws \Exception + */ + public function filterDirective($value) : string + { + return $this->_appState->emulateAreaCode( + \Magento\Framework\App\Area::AREA_FRONTEND, + [$this, 'filter'], + [$value] + ); + } } diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateAndSaveWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateAndSaveWidgetActionGroup.xml index 00f593a2d3bc8..5e564fcd799ae 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateAndSaveWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateAndSaveWidgetActionGroup.xml @@ -12,8 +12,8 @@ <annotations> <description>EXTENDS: AdminCreateWidgetActionGroup. Clicks on Save. Validates that the Success Message is present and correct.</description> </annotations> - + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateDynamicBlocksRotatorWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateDynamicBlocksRotatorWidgetActionGroup.xml new file mode 100644 index 0000000000000..16efe55202bb0 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateDynamicBlocksRotatorWidgetActionGroup.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="AdminCreateDynamicBlocksRotatorWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateWidgetActionGroup. Creates a Dynamic Block Rotate Widget.</description> + </annotations> + + <selectOption selector="{{AdminNewWidgetSection.displayMode}}" userInput="{{widget.display_mode}}" stepKey="selectDisplayMode"/> + <selectOption selector="{{AdminNewWidgetSection.restrictTypes}}" userInput="{{widget.restrict_type}}" stepKey="selectRestrictType"/> + <click selector="{{AdminNewWidgetSection.saveAndContinue}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateProductLinkWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateProductLinkWidgetActionGroup.xml new file mode 100644 index 0000000000000..cb82f5ad068fd --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateProductLinkWidgetActionGroup.xml @@ -0,0 +1,28 @@ +<?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="AdminCreateProductLinkWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateWidgetActionGroup. Creates a Product List Widget using the provided Product. Validates that the Success Message is present and correct.</description> + </annotations> + <arguments> + <argument name="product"/> + </arguments> + + <selectOption selector="{{AdminNewWidgetSection.selectTemplate}}" userInput="{{widget.template}}" after="waitForPageLoad" stepKey="setTemplate"/> + <waitForAjaxLoad after="setTemplate" stepKey="waitForPageLoad2"/> + <click selector="{{AdminNewWidgetSection.selectProduct}}" after="clickWidgetOptions" stepKey="clickSelectProduct"/> + <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" after="clickSelectProduct" stepKey="fillProductNameInFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" after="fillProductNameInFilter" stepKey="applyFilter"/> + <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" after="applyFilter" stepKey="selectProduct"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateProductsListWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateProductsListWidgetActionGroup.xml new file mode 100644 index 0000000000000..e3845adc9cd4a --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateProductsListWidgetActionGroup.xml @@ -0,0 +1,27 @@ +<?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="AdminCreateProductsListWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateWidgetActionGroup. Creates a Product List Widget. Validates that the Success Message is present and correct.</description> + </annotations> + + <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickAddNewCondition"/> + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{widget.condition}}" stepKey="selectCondition"/> + <waitForElement selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="waitRuleParameter"/> + <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickRuleParameter"/> + <click selector="{{AdminNewWidgetSection.openChooser}}" stepKey="clickChooser"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminNewWidgetSection.selectAll}}" stepKey="clickSelectAll"/> + <click selector="{{AdminNewWidgetSection.applyParameter}}" stepKey="clickApplyRuleParameter"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index 797abdb6f56ae..e657b3eb73b53 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -30,73 +30,4 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminNewWidgetSection.widgetOptions}}" stepKey="clickWidgetOptions"/> </actionGroup> - - <!--Create Product List Widget--> - <actionGroup name="AdminCreateProductsListWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> - <annotations> - <description>EXTENDS: AdminCreateWidgetActionGroup. Creates a Product List Widget. Validates that the Success Message is present and correct.</description> - </annotations> - - <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickAddNewCondition"/> - <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{widget.condition}}" stepKey="selectCondition"/> - <waitForElement selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="waitRuleParameter"/> - <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickRuleParameter"/> - <click selector="{{AdminNewWidgetSection.openChooser}}" stepKey="clickChooser"/> - <waitForAjaxLoad stepKey="waitForAjaxLoad"/> - <click selector="{{AdminNewWidgetSection.selectAll}}" stepKey="clickSelectAll"/> - <click selector="{{AdminNewWidgetSection.applyParameter}}" stepKey="clickApplyRuleParameter"/> - <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> - </actionGroup> - - <!--Create Dynamic Block Rotate Widget--> - <actionGroup name="AdminCreateDynamicBlocksRotatorWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> - <annotations> - <description>EXTENDS: AdminCreateWidgetActionGroup. Creates a Dynamic Block Rotate Widget.</description> - </annotations> - - <selectOption selector="{{AdminNewWidgetSection.displayMode}}" userInput="{{widget.display_mode}}" stepKey="selectDisplayMode"/> - <selectOption selector="{{AdminNewWidgetSection.restrictTypes}}" userInput="{{widget.restrict_type}}" stepKey="selectRestrictType"/> - <click selector="{{AdminNewWidgetSection.saveAndContinue}}" stepKey="clickSaveWidget"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> - </actionGroup> - - <actionGroup name="AdminDeleteWidgetActionGroup"> - <annotations> - <description>Goes to the Admin Widget grid page. Deletes the provided Widget. Validates that the Success Message is present and correct.</description> - </annotations> - <arguments> - <argument name="widget"/> - </arguments> - - <amOnPage url="{{AdminWidgetsPage.url}}" stepKey="amOnAdmin"/> - <waitForPageLoad stepKey="waitWidgetsLoad"/> - <fillField selector="{{AdminWidgetsSection.widgetTitleSearch}}" userInput="{{widget.name}}" stepKey="fillTitle"/> - <click selector="{{AdminWidgetsSection.searchButton}}" stepKey="clickContinue"/> - <click selector="{{AdminWidgetsSection.searchResult}}" stepKey="clickSearchResult"/> - <waitForPageLoad stepKey="waitForResultLoad"/> - <click selector="{{AdminMainActionsSection.delete}}" stepKey="clickDelete"/> - <waitForAjaxLoad stepKey="waitForAjaxLoad"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> - <waitForPageLoad stepKey="waitForDeleteLoad"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been deleted" stepKey="seeSuccess"/> - </actionGroup> - - <actionGroup name="AdminCreateProductLinkWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> - <annotations> - <description>EXTENDS: AdminCreateWidgetActionGroup. Creates a Product List Widget using the provided Product. Validates that the Success Message is present and correct.</description> - </annotations> - <arguments> - <argument name="product"/> - </arguments> - - <selectOption selector="{{AdminNewWidgetSection.selectTemplate}}" userInput="{{widget.template}}" after="waitForPageLoad" stepKey="setTemplate"/> - <waitForAjaxLoad after="setTemplate" stepKey="waitForPageLoad2"/> - <click selector="{{AdminNewWidgetSection.selectProduct}}" after="clickWidgetOptions" stepKey="clickSelectProduct"/> - <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" after="clickSelectProduct" stepKey="fillProductNameInFilter"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" after="fillProductNameInFilter" stepKey="applyFilter"/> - <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" after="applyFilter" stepKey="selectProduct"/> - <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> - <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminDeleteWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminDeleteWidgetActionGroup.xml new file mode 100644 index 0000000000000..889c8a9477534 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminDeleteWidgetActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteWidgetActionGroup"> + <annotations> + <description>Goes to the Admin Widget grid page. Deletes the provided Widget. Validates that the Success Message is present and correct.</description> + </annotations> + <arguments> + <argument name="widget"/> + </arguments> + + <amOnPage url="{{AdminWidgetsPage.url}}" stepKey="amOnAdmin"/> + <waitForPageLoad stepKey="waitWidgetsLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminWidgetsSection.widgetTitleSearch}}" userInput="{{widget.name}}" stepKey="fillTitle"/> + <click selector="{{AdminWidgetsSection.searchButton}}" stepKey="clickContinue"/> + <click selector="{{AdminWidgetsSection.searchResult}}" stepKey="clickSearchResult"/> + <waitForPageLoad stepKey="waitForResultLoad"/> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="clickDelete"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteLoad"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been deleted" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminFillSpecificPageWidgetMainFieldsActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminFillSpecificPageWidgetMainFieldsActionGroup.xml new file mode 100644 index 0000000000000..c0fa53da7c688 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminFillSpecificPageWidgetMainFieldsActionGroup.xml @@ -0,0 +1,36 @@ +<?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="AdminFillSpecificPageWidgetMainFieldsActionGroup"> + <annotations> + <description>Fill widget main fields and widget layout by index for specified page DisplayOn option</description> + </annotations> + <arguments> + <argument name="widget" type="entity" defaultValue="ProductsListWidget"/> + <argument name="index" type="string" defaultValue="0"/> + </arguments> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widget.type}}" stepKey="setWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widget.design_theme}}" stepKey="setWidgetDesignTheme"/> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" parameterArray="{{widget.store_ids}}" stepKey="setWidgetStoreIds"/> + <fillField selector="{{AdminNewWidgetSection.widgetSortOrder}}" userInput="{{widget.sort_order}}" stepKey="fillSortOrder"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.selectDisplayOn}}" stepKey="waitForSelectElement"/> + <selectOption selector="{{AdminNewWidgetSection.displayOnByIndex(index)}}" userInput="{{widget.display_on}}" stepKey="setDisplayOn"/> + <waitForPageLoad stepKey="waitForDisplayOnChangesApplied"/> + <selectOption selector="{{AdminNewWidgetSection.layoutByIndex(index)}}" userInput="{{widget.page}}" stepKey="selectPage"/> + <selectOption selector="{{AdminNewWidgetSection.templateByIndex(index)}}" userInput="{{widget.template}}" stepKey="selectTemplate"/> + <scrollTo selector="{{AdminNewWidgetSection.containerByIndex(index)}}" stepKey="scrollToSelectContainerElement"/> + <waitForPageLoad stepKey="waitForScroll"/> + <selectOption selector="{{AdminNewWidgetSection.containerByIndex(index)}}" userInput="{{widget.container}}" stepKey="setContainer"/> + <waitForPageLoad stepKey="waitForContainerChangesApplied"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index eebd6c10b5085..0777e6cbd58d9 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -11,12 +11,17 @@ <section name="AdminNewWidgetSection"> <element name="widgetType" type="select" selector="#code"/> <element name="widgetDesignTheme" type="select" selector="#theme_id"/> - <element name="continue" type="button" selector="#continue_button"/> + <element name="continue" type="button" timeout="30" selector="#continue_button"/> <element name="widgetTitle" type="input" selector="#title"/> <element name="widgetStoreIds" type="select" selector="#store_ids"/> + <element name="widgetSortOrder" type="input" selector="#sort_order"/> <element name="addLayoutUpdate" type="button" selector=".action-default.scalable.action-add"/> <element name="selectDisplayOn" type="select" selector="#widget_instance[0][page_group]"/> <element name="selectContainer" type="select" selector="#all_pages_0>table>tbody>tr>td:nth-child(1)>div>div>select"/> + <element name="displayOnByIndex" type="select" selector="select[name='widget_instance[{{index}}][page_group]']" parameterized="true"/> + <element name="layoutByIndex" type="select" selector="select[name='widget_instance[{{index}}][pages][layout_handle]']" parameterized="true"/> + <element name="containerByIndex" type="select" selector="select[name='widget_instance[{{index}}][pages][block]']" parameterized="true"/> + <element name="templateByIndex" type="select" selector="select[name='widget_instance[{{index}}][pages][template]']" parameterized="true"/> <element name="selectTemplate" type="select" selector=".widget-layout-updates .block_template_container .select"/> <element name="widgetOptions" type="select" selector="#widget_instace_tabs_properties_section"/> <element name="addNewCondition" type="select" selector=".rule-param.rule-param-new-child"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml index 2c4e2e70fec71..c63e76d851933 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml @@ -27,7 +27,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="logout"/> </after> <!-- Create a CMS page containing the New Products widget --> diff --git a/app/code/Magento/Widget/etc/db_schema.xml b/app/code/Magento/Widget/etc/db_schema.xml index a82e6aae20296..6146761f6f251 100644 --- a/app/code/Magento/Widget/etc/db_schema.xml +++ b/app/code/Magento/Widget/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="widget" resource="default" engine="innodb" comment="Preconfigured Widgets"> <column xsi:type="int" name="widget_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Widget Id"/> + comment="Widget ID"/> <column xsi:type="varchar" name="widget_code" nullable="true" length="255" comment="Widget code for template directive"/> <column xsi:type="varchar" name="widget_type" nullable="true" length="255" comment="Widget Type"/> @@ -23,10 +23,10 @@ </table> <table name="widget_instance" resource="default" engine="innodb" comment="Instances of Widget for Package Theme"> <column xsi:type="int" name="instance_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Instance Id"/> + comment="Instance ID"/> <column xsi:type="varchar" name="instance_type" nullable="true" length="255" comment="Instance Type"/> <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Theme id"/> + comment="Theme ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Widget Title"/> <column xsi:type="varchar" name="store_ids" nullable="false" length="255" default="0" comment="Store ids"/> <column xsi:type="text" name="widget_parameters" nullable="true" comment="Widget parameters"/> @@ -40,9 +40,9 @@ </table> <table name="widget_instance_page" resource="default" engine="innodb" comment="Instance of Widget on Page"> <column xsi:type="int" name="page_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Page Id"/> + comment="Page ID"/> <column xsi:type="int" name="instance_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Instance Id"/> + default="0" comment="Instance ID"/> <column xsi:type="varchar" name="page_group" nullable="true" length="25" comment="Block Group Type"/> <column xsi:type="varchar" name="layout_handle" nullable="true" length="255" comment="Layout Handle"/> <column xsi:type="varchar" name="block_reference" nullable="true" length="255" comment="Container"/> @@ -61,9 +61,9 @@ </table> <table name="widget_instance_page_layout" resource="default" engine="innodb" comment="Layout updates"> <column xsi:type="int" name="page_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Page Id"/> + comment="Page ID"/> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Layout Update Id"/> + default="0" comment="Layout Update ID"/> <constraint xsi:type="foreign" referenceId="WIDGET_INSTANCE_PAGE_LAYOUT_PAGE_ID_WIDGET_INSTANCE_PAGE_PAGE_ID" table="widget_instance_page_layout" column="page_id" referenceTable="widget_instance_page" referenceColumn="page_id" onDelete="CASCADE"/> @@ -80,7 +80,7 @@ </table> <table name="layout_update" resource="default" engine="innodb" comment="Layout Updates"> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Layout Update Id"/> + comment="Layout Update ID"/> <column xsi:type="varchar" name="handle" nullable="true" length="255" comment="Handle"/> <column xsi:type="text" name="xml" nullable="true" comment="Xml"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" @@ -96,13 +96,13 @@ </table> <table name="layout_link" resource="default" engine="innodb" comment="Layout Link"> <column xsi:type="int" name="layout_link_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Link Id"/> + comment="Link ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Theme id"/> + comment="Theme ID"/> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Layout Update Id"/> + default="0" comment="Layout Update ID"/> <column xsi:type="boolean" name="is_temporary" nullable="false" default="false" comment="Defines whether Layout Update is Temporary"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php index 63a480801d5d4..82133927e1201 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Options.php @@ -114,7 +114,7 @@ public function getConfiguredOptions() $option['value'][$key] = $this->escapeHtml($value); } } else { - $option['value'] = $this->escapeHtml($option['value']); + $option['value'] = $this->escapeHtml($option['value'], ["a"]); } } $options[$index]['value'] = $option['value']; diff --git a/app/code/Magento/Wishlist/Controller/Index/Add.php b/app/code/Magento/Wishlist/Controller/Index/Add.php index 5cb60905aea48..3ed152cb84125 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Add.php +++ b/app/code/Magento/Wishlist/Controller/Index/Add.php @@ -7,15 +7,18 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; /** + * Wish list Add controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Add extends \Magento\Wishlist\Controller\AbstractIndex +class Add extends \Magento\Wishlist\Controller\AbstractIndex implements HttpPostActionInterface { /** * @var \Magento\Wishlist\Controller\WishlistProviderInterface @@ -138,6 +141,7 @@ public function execute() 'referer' => $referer ] ); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addErrorMessage( __('We can\'t add the item to Wish List right now: %1.', $e->getMessage()) diff --git a/app/code/Magento/Wishlist/Controller/Index/Cart.php b/app/code/Magento/Wishlist/Controller/Index/Cart.php index da37609d688e7..870c4231f97c9 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Cart.php @@ -186,7 +186,7 @@ public function execute() 'You added %1 to your shopping cart.', $this->escaper->escapeHtml($item->getProduct()->getName()) ); - $this->messageManager->addSuccess($message); + $this->messageManager->addSuccessMessage($message); } if ($this->cartHelper->getShouldRedirectToCart()) { @@ -214,7 +214,7 @@ public function execute() $resultJson->setData(['backUrl' => $redirectUrl]); return $resultJson; } - + $resultRedirect->setUrl($redirectUrl); return $resultRedirect; } diff --git a/app/code/Magento/Wishlist/Controller/Index/Configure.php b/app/code/Magento/Wishlist/Controller/Index/Configure.php index 93a05ffc0ec93..a273a37deeff3 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Configure.php +++ b/app/code/Magento/Wishlist/Controller/Index/Configure.php @@ -11,9 +11,11 @@ use Magento\Framework\Controller\ResultFactory; /** + * Wishlist Configure Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Configure extends \Magento\Wishlist\Controller\AbstractIndex +class Configure extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpGetActionInterface { /** * Core registry @@ -102,11 +104,11 @@ public function execute() return $resultPage; } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('*'); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addError(__('We can\'t configure the product right now.')); + $this->messageManager->addErrorMessage(__('We can\'t configure the product right now.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); $resultRedirect->setPath('*'); return $resultRedirect; diff --git a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php index 742b2a91e9317..dc0ea8d5a093a 100644 --- a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php +++ b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php @@ -99,7 +99,7 @@ public function execute() $this->_fileResponseFactory->create( $info['title'], ['value' => $info['quote_path'], 'type' => 'filename'], - DirectoryList::ROOT, + DirectoryList::MEDIA, $info['type'] ); } diff --git a/app/code/Magento/Wishlist/Controller/Index/Plugin.php b/app/code/Magento/Wishlist/Controller/Index/Plugin.php index 60d6859613d5e..150e4de72b40d 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Plugin.php +++ b/app/code/Magento/Wishlist/Controller/Index/Plugin.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -12,6 +11,9 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\RedirectInterface; +/** + * Wishlist plugin before dispatch + */ class Plugin { /** @@ -75,7 +77,9 @@ public function beforeDispatch(\Magento\Framework\App\ActionInterface $subject, if (!$this->customerSession->getBeforeWishlistUrl()) { $this->customerSession->setBeforeWishlistUrl($this->redirector->getRefererUrl()); } - $this->customerSession->setBeforeWishlistRequest($request->getParams()); + $data = $request->getParams(); + unset($data['login']); + $this->customerSession->setBeforeWishlistRequest($data); $this->customerSession->setBeforeRequestParams($this->customerSession->getBeforeWishlistRequest()); $this->customerSession->setBeforeModuleName('wishlist'); $this->customerSession->setBeforeControllerName('index'); diff --git a/app/code/Magento/Wishlist/Controller/Index/Remove.php b/app/code/Magento/Wishlist/Controller/Index/Remove.php index 84c59b5be3d1e..ea798a8b57cda 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Remove.php +++ b/app/code/Magento/Wishlist/Controller/Index/Remove.php @@ -14,9 +14,11 @@ use Magento\Wishlist\Model\Product\AttributeValueProvider; /** + * Wishlist Remove Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Remove extends \Magento\Wishlist\Controller\AbstractIndex +class Remove extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var WishlistProviderInterface @@ -88,11 +90,11 @@ public function execute() ] ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('We can\'t delete the item from Wish List right now because of an error: %1.', $e->getMessage()) ); } catch (\Exception $e) { - $this->messageManager->addError(__('We can\'t delete the item from the Wish List right now.')); + $this->messageManager->addErrorMessage(__('We can\'t delete the item from the Wish List right now.')); } $this->_objectManager->get(\Magento\Wishlist\Helper\Data::class)->calculate(); diff --git a/app/code/Magento/Wishlist/Controller/Index/Send.php b/app/code/Magento/Wishlist/Controller/Index/Send.php index 5d867ac74752b..54aa53d829db5 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Send.php +++ b/app/code/Magento/Wishlist/Controller/Index/Send.php @@ -219,7 +219,7 @@ public function execute() } if ($error) { - $this->messageManager->addError($error); + $this->messageManager->addErrorMessage($error); $this->wishlistSession->setSharingForm($this->getRequest()->getPostValue()); $resultRedirect->setPath('*/*/share'); return $resultRedirect; @@ -285,12 +285,12 @@ public function execute() $this->inlineTranslation->resume(); $this->_eventManager->dispatch('wishlist_share', ['wishlist' => $wishlist]); - $this->messageManager->addSuccess(__('Your wish list has been shared.')); + $this->messageManager->addSuccessMessage(__('Your wish list has been shared.')); $resultRedirect->setPath('*/*', ['wishlist_id' => $wishlist->getId()]); return $resultRedirect; } catch (\Exception $e) { $this->inlineTranslation->resume(); - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->wishlistSession->setSharingForm($this->getRequest()->getPostValue()); $resultRedirect->setPath('*/*/share'); return $resultRedirect; diff --git a/app/code/Magento/Wishlist/Controller/Index/Update.php b/app/code/Magento/Wishlist/Controller/Index/Update.php index b56aa4b5b3c8d..e5fbd4b93f82e 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Update.php +++ b/app/code/Magento/Wishlist/Controller/Index/Update.php @@ -103,7 +103,7 @@ public function execute() $item->delete(); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); - $this->messageManager->addError(__('We can\'t delete item from Wish List right now.')); + $this->messageManager->addErrorMessage(__('We can\'t delete item from Wish List right now.')); } } @@ -118,7 +118,7 @@ public function execute() ); $updatedItems++; } catch (\Exception $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( 'Can\'t save description %1', $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($description) @@ -133,7 +133,7 @@ public function execute() $wishlist->save(); $this->_objectManager->get(\Magento\Wishlist\Helper\Data::class)->calculate(); } catch (\Exception $e) { - $this->messageManager->addError(__('Can\'t update wish list')); + $this->messageManager->addErrorMessage(__('Can\'t update wish list')); } } diff --git a/app/code/Magento/Wishlist/Controller/Index/UpdateItemOptions.php b/app/code/Magento/Wishlist/Controller/Index/UpdateItemOptions.php index 2a98fa1b7fcd5..6fae77fd604e5 100644 --- a/app/code/Magento/Wishlist/Controller/Index/UpdateItemOptions.php +++ b/app/code/Magento/Wishlist/Controller/Index/UpdateItemOptions.php @@ -14,9 +14,11 @@ use Magento\Wishlist\Controller\WishlistProviderInterface; /** + * Wishlist UpdateItemOptions Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class UpdateItemOptions extends \Magento\Wishlist\Controller\AbstractIndex +class UpdateItemOptions extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var WishlistProviderInterface @@ -85,7 +87,7 @@ public function execute() } if (!$product || !$product->isVisibleInCatalog()) { - $this->messageManager->addError(__('We can\'t specify a product.')); + $this->messageManager->addErrorMessage(__('We can\'t specify a product.')); $resultRedirect->setPath('*/'); return $resultRedirect; } @@ -114,11 +116,11 @@ public function execute() $this->_objectManager->get(\Magento\Wishlist\Helper\Data::class)->calculate(); $message = __('%1 has been updated in your Wish List.', $product->getName()); - $this->messageManager->addSuccess($message); + $this->messageManager->addSuccessMessage($message); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('We can\'t update your Wish List right now.')); + $this->messageManager->addErrorMessage(__('We can\'t update your Wish List right now.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } $resultRedirect->setPath('*/*', ['wishlist_id' => $wishlist->getId()]); diff --git a/app/code/Magento/Wishlist/Controller/Shared/Cart.php b/app/code/Magento/Wishlist/Controller/Shared/Cart.php index b41b51057636f..38f100602972a 100644 --- a/app/code/Magento/Wishlist/Controller/Shared/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Shared/Cart.php @@ -9,6 +9,7 @@ use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart as CustomerCart; use Magento\Framework\App\Action\Context as ActionContext; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\LocalizedException; @@ -18,9 +19,11 @@ use Magento\Wishlist\Model\ResourceModel\Item\Option\Collection as OptionCollection; /** + * Wishlist Cart Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Cart extends \Magento\Framework\App\Action\Action +class Cart extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * @var CustomerCart @@ -103,19 +106,19 @@ public function execute() 'You added %1 to your shopping cart.', $this->escaper->escapeHtml($item->getProduct()->getName()) ); - $this->messageManager->addSuccess($message); + $this->messageManager->addSuccessMessage($message); } if ($this->cartHelper->getShouldRedirectToCart()) { $redirectUrl = $this->cartHelper->getCartUrl(); } } catch (ProductException $e) { - $this->messageManager->addError(__('This product(s) is out of stock.')); + $this->messageManager->addErrorMessage(__('This product(s) is out of stock.')); } catch (LocalizedException $e) { - $this->messageManager->addNotice($e->getMessage()); + $this->messageManager->addNoticeMessage($e->getMessage()); $redirectUrl = $item->getProductUrl(); } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t add the item to the cart right now.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t add the item to the cart right now.')); } /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Wishlist/Controller/WishlistProvider.php b/app/code/Magento/Wishlist/Controller/WishlistProvider.php index 4740ea9947ef7..ae59d3a13d6eb 100644 --- a/app/code/Magento/Wishlist/Controller/WishlistProvider.php +++ b/app/code/Magento/Wishlist/Controller/WishlistProvider.php @@ -1,13 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Wishlist\Controller; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\RequestInterface; +/** + * WishlistProvider Controller + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class WishlistProvider implements WishlistProviderInterface { /** @@ -54,7 +59,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getWishlist($wishlistId = null) @@ -85,10 +91,10 @@ public function getWishlist($wishlistId = null) ); } } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); return false; } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t create the Wish List right now.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t create the Wish List right now.')); return false; } $this->wishlist = $wishlist; diff --git a/app/code/Magento/Wishlist/Helper/Data.php b/app/code/Magento/Wishlist/Helper/Data.php index 2984d68f8deeb..e8280db3e1f21 100644 --- a/app/code/Magento/Wishlist/Helper/Data.php +++ b/app/code/Magento/Wishlist/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\Wishlist\Helper; use Magento\Framework\App\ActionInterface; @@ -117,6 +120,9 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Customer\Helper\View $customerViewHelper * @param WishlistProviderInterface $wishlistProvider * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param Escaper $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -127,7 +133,8 @@ public function __construct( \Magento\Framework\Data\Helper\PostHelper $postDataHelper, \Magento\Customer\Helper\View $customerViewHelper, WishlistProviderInterface $wishlistProvider, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, + Escaper $escaper = null ) { $this->_coreRegistry = $coreRegistry; $this->_customerSession = $customerSession; @@ -137,7 +144,7 @@ public function __construct( $this->_customerViewHelper = $customerViewHelper; $this->wishlistProvider = $wishlistProvider; $this->productRepository = $productRepository; - $this->escaper = ObjectManager::getInstance()->get(Escaper::class); + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); parent::__construct($context); } @@ -352,7 +359,6 @@ public function getAddParams($item, array $params = []) * Retrieve params for adding product to wishlist * * @param int $itemId - * * @return string */ public function getMoveFromCartParams($itemId) @@ -366,7 +372,6 @@ public function getMoveFromCartParams($itemId) * Retrieve params for updating product in wishlist * * @param \Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item - * * @return string|false */ public function getUpdateParams($item) @@ -541,6 +546,7 @@ public function getCustomerName() */ public function getRssUrl($wishlistId = null) { + $params = []; $customer = $this->_getCurrentCustomer(); if ($customer) { $key = $customer->getId() . ',' . $customer->getEmail(); @@ -574,6 +580,7 @@ public function getDefaultWishlistName() /** * Calculate count of wishlist items and put value to customer session. + * * Method called after wishlist modifications and trigger 'wishlist_items_renewed' event. * Depends from configuration. * diff --git a/app/code/Magento/Wishlist/Model/ItemCarrier.php b/app/code/Magento/Wishlist/Model/ItemCarrier.php index 6cf295084eca8..6198eb289a0f4 100644 --- a/app/code/Magento/Wishlist/Model/ItemCarrier.php +++ b/app/code/Magento/Wishlist/Model/ItemCarrier.php @@ -10,6 +10,7 @@ use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart; use Magento\Customer\Model\Session; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Response\RedirectInterface; use Magento\Framework\Exception\LocalizedException; use Psr\Log\LoggerInterface as Logger; @@ -18,7 +19,10 @@ use Magento\Wishlist\Helper\Data as WishlistHelper; /** + * Wishlist ItemCarrier Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class ItemCarrier { @@ -182,7 +186,7 @@ public function moveAllToCart(Wishlist $wishlist, $qtys) if ($messages) { foreach ($messages as $message) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } $redirectUrl = $indexUrl; } @@ -192,7 +196,7 @@ public function moveAllToCart(Wishlist $wishlist, $qtys) try { $wishlist->save(); } catch (\Exception $e) { - $this->messageManager->addError(__('We can\'t update the Wish List right now.')); + $this->messageManager->addErrorMessage(__('We can\'t update the Wish List right now.')); $redirectUrl = $indexUrl; } @@ -202,7 +206,7 @@ public function moveAllToCart(Wishlist $wishlist, $qtys) $products[] = '"' . $product->getName() . '"'; } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('%1 product(s) have been added to shopping cart: %2.', count($addedProducts), join(', ', $products)) ); diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php index fb6b647811abb..6ef55bbe81b73 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php @@ -6,10 +6,12 @@ namespace Magento\Wishlist\Model\ResourceModel\Item\Collection; -use Magento\Customer\Controller\RegistryConstants as RegistryConstants; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Wishlist\Model\Item; /** - * Wishlist item collection grouped by customer id + * Wishlist item collection for grid grouped by customer id * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -88,30 +90,48 @@ public function __construct( } /** - * Initialize db select - * - * @return $this + * @inheritdoc */ protected function _initSelect() { parent::_initSelect(); - $this->addCustomerIdFilter( - $this->_registryManager->registry(RegistryConstants::CURRENT_CUSTOMER_ID) - ) - ->resetSortOrder() - ->addDaysInWishlist() + + $customerId = $this->_registryManager->registry(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->addDaysInWishlist() ->addStoreData() - ->setVisibilityFilter() - ->setInStockFilter(); + ->addCustomerIdFilter($customerId) + ->resetSortOrder(); + return $this; } /** - * Add select order - * - * @param string $field - * @param string $direction - * @return \Magento\Framework\Data\Collection\AbstractDb + * @inheritdoc + */ + protected function _assignProducts() + { + /** @var ProductCollection $productCollection */ + $productCollection = $this->_productCollectionFactory->create() + ->addAttributeToSelect($this->_wishlistConfig->getProductAttributes()) + ->addIdFilter($this->_productIds); + + /** @var Item $item */ + foreach ($this as $item) { + $product = $productCollection->getItemById($item->getProductId()); + if ($product) { + $product->setCustomOptions([]); + $item->setProduct($product); + $item->setProductName($product->getName()); + $item->setName($product->getName()); + $item->setPrice($product->getPrice()); + } + } + + return $this; + } + + /** + * @inheritdoc */ public function setOrder($field, $direction = self::SORT_ORDER_DESC) { @@ -127,24 +147,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) } /** - * Add quantity to filter - * - * @param string $field - * @param array $condition - * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection - */ - private function addQtyFilter(string $field, array $condition) - { - return parent::addFieldToFilter('main_table.' . $field, $condition); - } - - /** - * Add field filter to collection - * - * @param string|array $field - * @param null|string|array $condition - * @see self::_getConditionSql for $condition - * @return \Magento\Framework\Data\Collection\AbstractDb + * @inheritdoc */ public function addFieldToFilter($field, $condition = null) { @@ -168,6 +171,19 @@ public function addFieldToFilter($field, $condition = null) return $this->addQtyFilter($field, $condition); } } + return parent::addFieldToFilter($field, $condition); } + + /** + * Add quantity to filter + * + * @param string $field + * @param array $condition + * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + private function addQtyFilter(string $field, array $condition) + { + return parent::addFieldToFilter('main_table.' . $field, $condition); + } } diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index 9797ab58b0766..9b7ff5177afae 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -7,10 +7,30 @@ namespace Magento\Wishlist\Model; +use Exception; +use InvalidArgumentException; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductFactory; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Math\Random; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Wishlist\Helper\Data; use Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory; use Magento\Wishlist\Model\ResourceModel\Wishlist as ResourceWishlist; use Magento\Wishlist\Model\ResourceModel\Wishlist\Collection; @@ -19,21 +39,21 @@ * Wishlist model * * @method int getShared() - * @method \Magento\Wishlist\Model\Wishlist setShared(int $value) + * @method Wishlist setShared(int $value) * @method string getSharingCode() - * @method \Magento\Wishlist\Model\Wishlist setSharingCode(string $value) + * @method Wishlist setSharingCode(string $value) * @method string getUpdatedAt() - * @method \Magento\Wishlist\Model\Wishlist setUpdatedAt(string $value) + * @method Wishlist setUpdatedAt(string $value) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * * @api * @since 100.0.2 */ -class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magento\Framework\DataObject\IdentityInterface +class Wishlist extends AbstractModel implements IdentityInterface { /** - * Cache tag + * Wishlist cache tag name */ const CACHE_TAG = 'wishlist'; @@ -47,14 +67,14 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent /** * Wishlist item collection * - * @var \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @var ResourceModel\Item\Collection */ protected $_itemCollection; /** * Store filter for wishlist * - * @var \Magento\Store\Model\Store + * @var Store */ protected $_store; @@ -68,7 +88,7 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent /** * Wishlist data * - * @var \Magento\Wishlist\Helper\Data + * @var Data */ protected $_wishlistData; @@ -80,12 +100,12 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent protected $_catalogProduct; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime + * @var DateTime\DateTime */ protected $_date; @@ -100,17 +120,17 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent protected $_wishlistCollectionFactory; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $_productFactory; /** - * @var \Magento\Framework\Math\Random + * @var Random */ protected $mathRandom; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $dateTime; @@ -129,46 +149,60 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent */ private $serializer; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StockRegistryInterface|null + */ + private $stockRegistry; + /** * Constructor * - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry + * @param Context $context + * @param Registry $registry * @param \Magento\Catalog\Helper\Product $catalogProduct - * @param \Magento\Wishlist\Helper\Data $wishlistData + * @param Data $wishlistData * @param ResourceWishlist $resource * @param Collection $resourceCollection - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Stdlib\DateTime\DateTime $date + * @param StoreManagerInterface $storeManager + * @param DateTime\DateTime $date * @param ItemFactory $wishlistItemFactory * @param CollectionFactory $wishlistCollectionFactory - * @param \Magento\Catalog\Model\ProductFactory $productFactory - * @param \Magento\Framework\Math\Random $mathRandom - * @param \Magento\Framework\Stdlib\DateTime $dateTime + * @param ProductFactory $productFactory + * @param Random $mathRandom + * @param DateTime $dateTime * @param ProductRepositoryInterface $productRepository * @param bool $useCurrentWebsite * @param array $data * @param Json|null $serializer + * @param StockRegistryInterface|null $stockRegistry + * @param ScopeConfigInterface|null $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\Context $context, - \Magento\Framework\Registry $registry, + Context $context, + Registry $registry, \Magento\Catalog\Helper\Product $catalogProduct, - \Magento\Wishlist\Helper\Data $wishlistData, + Data $wishlistData, ResourceWishlist $resource, Collection $resourceCollection, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Stdlib\DateTime\DateTime $date, + StoreManagerInterface $storeManager, + DateTime\DateTime $date, ItemFactory $wishlistItemFactory, CollectionFactory $wishlistCollectionFactory, - \Magento\Catalog\Model\ProductFactory $productFactory, - \Magento\Framework\Math\Random $mathRandom, - \Magento\Framework\Stdlib\DateTime $dateTime, + ProductFactory $productFactory, + Random $mathRandom, + DateTime $dateTime, ProductRepositoryInterface $productRepository, $useCurrentWebsite = true, array $data = [], - Json $serializer = null + Json $serializer = null, + StockRegistryInterface $stockRegistry = null, + ScopeConfigInterface $scopeConfig = null ) { $this->_useCurrentWebsite = $useCurrentWebsite; $this->_catalogProduct = $catalogProduct; @@ -183,6 +217,8 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->productRepository = $productRepository; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->stockRegistry = $stockRegistry ?: ObjectManager::getInstance()->get(StockRegistryInterface::class); } /** @@ -290,13 +326,13 @@ public function afterSave() /** * Add catalog product object data to wishlist * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param int $qty * @param bool $forciblySetQty * * @return Item */ - protected function _addCatalogProduct(\Magento\Catalog\Model\Product $product, $qty = 1, $forciblySetQty = false) + protected function _addCatalogProduct(Product $product, $qty = 1, $forciblySetQty = false) { $item = null; foreach ($this->getItemCollection() as $_item) { @@ -311,7 +347,7 @@ protected function _addCatalogProduct(\Magento\Catalog\Model\Product $product, $ $item = $this->_wishlistItemFactory->create(); $item->setProductId($product->getId()); $item->setWishlistId($this->getId()); - $item->setAddedAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)); + $item->setAddedAt((new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT)); $item->setStoreId($storeId); $item->setOptions($product->getCustomOptions()); $item->setProduct($product); @@ -334,6 +370,7 @@ protected function _addCatalogProduct(\Magento\Catalog\Model\Product $product, $ * Retrieve wishlist item collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @throws NoSuchEntityException */ public function getItemCollection() { @@ -365,8 +402,9 @@ public function getItem($itemId) /** * Adding item to wishlist * - * @param Item $item - * @return $this + * @param Item $item + * @return $this + * @throws Exception */ public function addItem(Item $item) { @@ -383,13 +421,14 @@ public function addItem(Item $item) * * Returns new item or string on error. * - * @param int|\Magento\Catalog\Model\Product $product - * @param \Magento\Framework\DataObject|array|string|null $buyRequest + * @param int|Product $product + * @param DataObject|array|string|null $buyRequest * @param bool $forciblySetQty - * @throws \Magento\Framework\Exception\LocalizedException * @return Item|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws LocalizedException + * @throws InvalidArgumentException */ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false) { @@ -398,7 +437,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * a) we have new instance and do not interfere with other products in wishlist * b) product has full set of attributes */ - if ($product instanceof \Magento\Catalog\Model\Product) { + if ($product instanceof Product) { $productId = $product->getId(); // Maybe force some store by wishlist internal properties $storeId = $product->hasWishlistStoreId() ? $product->getWishlistStoreId() : $product->getStoreId(); @@ -412,12 +451,17 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false } try { + /** @var Product $product */ $product = $this->productRepository->getById($productId, false, $storeId); } catch (NoSuchEntityException $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('Cannot specify product.')); + throw new LocalizedException(__('Cannot specify product.')); + } + + if ($this->isInStock($productId)) { + throw new LocalizedException(__('Cannot add product without stock to wishlist.')); } - if ($buyRequest instanceof \Magento\Framework\DataObject) { + if ($buyRequest instanceof DataObject) { $_buyRequest = $buyRequest; } elseif (is_string($buyRequest)) { $isInvalidItemConfiguration = false; @@ -426,20 +470,20 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false if (!is_array($buyRequestData)) { $isInvalidItemConfiguration = true; } - } catch (\InvalidArgumentException $exception) { + } catch (Exception $exception) { $isInvalidItemConfiguration = true; } if ($isInvalidItemConfiguration) { - throw new \InvalidArgumentException('Invalid wishlist item configuration.'); + throw new InvalidArgumentException('Invalid wishlist item configuration.'); } - $_buyRequest = new \Magento\Framework\DataObject($buyRequestData); + $_buyRequest = new DataObject($buyRequestData); } elseif (is_array($buyRequest)) { - $_buyRequest = new \Magento\Framework\DataObject($buyRequest); + $_buyRequest = new DataObject($buyRequest); } else { - $_buyRequest = new \Magento\Framework\DataObject(); + $_buyRequest = new DataObject(); } - /* @var $product \Magento\Catalog\Model\Product */ + /* @var $product Product */ $cartCandidates = $product->getTypeInstance()->processConfiguration($_buyRequest, clone $product); /** @@ -486,6 +530,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * * @param int $customerId * @return $this + * @throws LocalizedException */ public function setCustomerId($customerId) { @@ -496,6 +541,7 @@ public function setCustomerId($customerId) * Retrieve customer id * * @return int + * @throws LocalizedException */ public function getCustomerId() { @@ -506,6 +552,7 @@ public function getCustomerId() * Retrieve data for save * * @return array + * @throws LocalizedException */ public function getDataForSave() { @@ -520,6 +567,7 @@ public function getDataForSave() * Retrieve shared store ids for current website or all stores if $current is false * * @return array + * @throws NoSuchEntityException */ public function getSharedStoreIds() { @@ -554,6 +602,7 @@ public function setSharedStoreIds($storeIds) * Retrieve wishlist store object * * @return \Magento\Store\Model\Store + * @throws NoSuchEntityException */ public function getStore() { @@ -566,7 +615,7 @@ public function getStore() /** * Set wishlist store * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return $this */ public function setStore($store) @@ -600,11 +649,30 @@ public function isSalable() return false; } + /** + * Retrieve if product has stock or config is set for showing out of stock products + * + * @param int $productId + * @return bool + */ + private function isInStock($productId) + { + /** @var StockItemInterface $stockItem */ + $stockItem = $this->stockRegistry->getStockItem($productId); + $showOutOfStock = $this->scopeConfig->isSetFlag( + Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + ScopeInterface::SCOPE_STORE + ); + $isInStock = $stockItem ? $stockItem->getIsInStock() : false; + return !$isInStock && !$showOutOfStock; + } + /** * Check customer is owner this wishlist * * @param int $customerId * @return bool + * @throws LocalizedException */ public function isOwner($customerId) { @@ -626,10 +694,10 @@ public function isOwner($customerId) * For more options see \Magento\Catalog\Helper\Product->addParamsToBuyRequest() * * @param int|Item $itemId - * @param \Magento\Framework\DataObject $buyRequest - * @param null|array|\Magento\Framework\DataObject $params + * @param DataObject $buyRequest + * @param null|array|DataObject $params * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * * @see \Magento\Catalog\Helper\Product::addParamsToBuyRequest() * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -645,16 +713,16 @@ public function updateItem($itemId, $buyRequest, $params = null) $item = $this->getItem((int)$itemId); } if (!$item) { - throw new \Magento\Framework\Exception\LocalizedException(__('We can\'t specify a wish list item.')); + throw new LocalizedException(__('We can\'t specify a wish list item.')); } $product = $item->getProduct(); $productId = $product->getId(); if ($productId) { if (!$params) { - $params = new \Magento\Framework\DataObject(); + $params = new DataObject(); } elseif (is_array($params)) { - $params = new \Magento\Framework\DataObject($params); + $params = new DataObject($params); } $params->setCurrentConfig($item->getBuyRequest()); $buyRequest = $this->_catalogProduct->addParamsToBuyRequest($buyRequest, $params); @@ -677,7 +745,7 @@ public function updateItem($itemId, $buyRequest, $params = null) * Error message */ if (is_string($resultItem)) { - throw new \Magento\Framework\Exception\LocalizedException(__($resultItem)); + throw new LocalizedException(__($resultItem)); } if ($resultItem->getId() != $itemId) { @@ -691,7 +759,7 @@ public function updateItem($itemId, $buyRequest, $params = null) $resultItem->setOrigData('qty', 0); } } else { - throw new \Magento\Framework\Exception\LocalizedException(__('The product does not exist.')); + throw new LocalizedException(__('The product does not exist.')); } return $this; } diff --git a/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php b/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php deleted file mode 100644 index 5c65fce10ccd2..0000000000000 --- a/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Wishlist\Setup\Patch\Schema; - -use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Class AddProductIdConstraint - */ -class AddProductIdConstraint implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct( - SchemaSetupInterface $schemaSetup - ) { - $this->schemaSetup = $schemaSetup; - } - - /** - * Run code inside patch. - * - * @return void - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $this->schemaSetup->getConnection()->addForeignKey( - $this->schemaSetup->getConnection()->getForeignKeyName( - $this->schemaSetup->getTable('wishlist_item_option'), - 'product_id', - $this->schemaSetup->getTable('catalog_product_entity'), - 'entity_id' - ), - $this->schemaSetup->getTable('wishlist_item_option'), - 'product_id', - $this->schemaSetup->getTable('catalog_product_entity'), - 'entity_id', - AdapterInterface::FK_ACTION_CASCADE, - true - ); - - $this->schemaSetup->endSetup(); - } - - /** - * Get array of patches that have to be executed prior to this. - * - * @return string[] - */ - public static function getDependencies() - { - return []; - } - - /** - * Get aliases (previous names) for the patch. - * - * @return string[] - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml index cda7f5f3267b0..5568f255304a7 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml @@ -33,7 +33,7 @@ <argument name="productVar"/> </arguments> - <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productAddToWishlist}}" stepKey="WaitForWishList"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productAddToWishlist}}" stepKey="WaitForWishList" time="30"/> <click selector="{{StorefrontProductInfoMainSection.productAddToWishlist}}" stepKey="addProductToWishlistClickAddToWishlist"/> <waitForElement selector="{{StorefrontCustomerWishlistSection.successMsg}}" time="30" stepKey="addProductToWishlistWaitForSuccessMessage"/> <see selector="{{StorefrontCustomerWishlistSection.successMsg}}" userInput="{{productVar.name}} has been added to your Wish List. Click here to continue shopping." stepKey="addProductToWishlistSeeProductNameAddedToWishlist"/> @@ -135,4 +135,47 @@ <dontSeeElement selector="{{StorefrontCustomerWishlistProductSection.pager}}" stepKey="checkThatPagerIsAbsent"/> <see selector="{{StorefrontCustomerWishlistProductSection.wishlistEmpty}}" userInput="You have no items in your wish list." stepKey="checkNoItemsMessage"/> </actionGroup> + + <actionGroup name="AssertMoveProductToWishListSuccessMessage"> + <annotations> + <description>Moves a product from the cart to the wishlist.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{CheckoutCartProductSection.moveToWishlistByProductName(productName)}}" stepKey="moveToWishlist"/> + <waitForPageLoad stepKey="waitForMove"/> + <see userInput="{{productName}} has been moved to your wish list." selector="{{CheckoutCartMessageSection.successMessage}}" stepKey="assertSuccess"/> + </actionGroup> + + <actionGroup name="AssertProductIsPresentInWishList"> + <annotations> + <description>Go to storefront customer wishlist page and assert product name and price is present.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + <argument name="productPrice" type="string"/> + </arguments> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="goToWishList"/> + <waitForPageLoad stepKey="waitForWishList"/> + <waitForElement selector="{{StorefrontCustomerWishlistProductSection.ProductTitleByName(productName)}}" time="30" stepKey="assertProductName"/> + <see userInput="{{productPrice}}" selector="{{StorefrontCustomerWishlistProductSection.ProductPriceByName(productName)}}" stepKey="assertProductPrice"/> + </actionGroup> + + <actionGroup name="AssertProductDetailsInWishlist"> + <annotations> + <description>Assert product name and price in wishlist on hover.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + <argument name="label" type="string"/> + <argument name="labelValue" type="string"/> + </arguments> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName(productName)}}" stepKey="moveMouseOverProductInfo"/> + <seeElement selector="{{StorefrontCustomerWishlistProductSection.ProductAddToCartByName(productName)}}" stepKey="seeAddToCart"/> + <seeElement selector="{{StorefrontCustomerWishlistProductSection.ProductImageByName(productName)}}" stepKey="seeImage"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productSeeDetailsByName(productName)}}" stepKey="moveMouseOverProductDetails"/> + <see userInput="{{label}}" selector="{{StorefrontCustomerWishlistProductSection.productSeeDetailsLabelByName(productName)}}" stepKey="seeLabel"/> + <see userInput="{{labelValue}}" selector="{{StorefrontCustomerWishlistProductSection.productSeeDetailsValueByName(productName)}}" stepKey="seeLabelValue"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml index 0b6c2f1191c40..e75b29944117b 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml @@ -24,5 +24,9 @@ <element name="productSuccessShareMessage" type="text" selector="div.message-success"/> <element name="pager" type="block" selector=".toolbar .pager"/> <element name="wishlistEmpty" type="block" selector=".form-wishlist-items .message.info.empty"/> + <element name="removeProduct" type="button" selector=".products-grid a.btn-remove" timeout="30"/> + <element name="productSeeDetailsByName" type="block" selector="//a[contains(text(), '{{productName}}')]/ancestor::div/div[contains(@class, 'product-item-tooltip')]" parameterized="true"/> + <element name="productSeeDetailsLabelByName" type="block" selector="//a[contains(text(), '{{productName}}')]/ancestor::div/div[contains(@class, 'product-item-tooltip')]//dt[@class='label']" parameterized="true"/> + <element name="productSeeDetailsValueByName" type="block" selector="//a[contains(text(), '{{productName}}')]/ancestor::div/div[contains(@class, 'product-item-tooltip')]//dd[@class='values']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml index 6b951c89208c2..0489ec750b7e0 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -32,7 +32,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index e8b645990390e..aeb1d134d8e22 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -48,7 +48,7 @@ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsFilters"/> <!--Logout everywhere--> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> </after> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml index 16a18dd27b123..82c53bc343e51 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml @@ -26,12 +26,16 @@ <requiredEntity createDataKey="categorySecond"/> </createData> <createData entity="Simple_US_Customer" stepKey="customer"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="categoryFirst" stepKey="deleteCategoryFirst"/> <deleteData createDataKey="categorySecond" stepKey="deleteCategorySecond"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <!-- Sign in as customer --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleDynamicProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleDynamicProductFromWishlistTest.xml new file mode 100644 index 0000000000000..ae65a4171d883 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleDynamicProductFromWishlistTest.xml @@ -0,0 +1,95 @@ +<?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="StorefrontDeleteBundleDynamicProductFromWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Delete Dynamic Bundle Product from Wishlist on Frontend"/> + <description value="Delete Dynamic Bundle Product from Wishlist on Frontend"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14215"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">100.00</field> + </createData> + <!--Create Bundle product--> + <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">True</field> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="goToProductPageViaID" stepKey="goToProduct"> + <argument name="productId" value="$$createBundleProduct.id$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductFormBundleSection.contentDropDown}}" stepKey="scrollToBundleSection"/> + <selectOption userInput="Separately" selector="{{AdminProductFormBundleSection.shipmentType}}" stepKey="selectSeparately"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Navigate to catalog page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$$createBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Add created product to Wishlist according to dataset and assert add product to wishlist success message --> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$createBundleProduct$$"/> + </actionGroup> + + <!-- Navigate to My Account > My Wishlist --> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishListPage"/> + <waitForPageLoad stepKey="waitForWishlistPageLoad"/> + + <!-- Click "Remove item" --> + <scrollTo selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName($$createBundleProduct.name$$)}}" stepKey="scrollToProduct"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName($$createBundleProduct.name$$)}}" stepKey="mouseOverOnProduct"/> + <click selector="{{StorefrontCustomerWishlistProductSection.removeProduct}}" stepKey="clickRemoveButton"/> + + <!-- Assert Wishlist is empty --> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmpty" stepKey="assertWishlistIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml new file mode 100644 index 0000000000000..a0bff949f00f5 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml @@ -0,0 +1,88 @@ +<?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="StorefrontDeleteBundleFixedProductFromWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Delete Fixed Bundle Product from Wishlist on Frontend"/> + <description value="Delete Fixed Bundle Product from Wishlist on Frontend"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14216"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">100.00</field> + </createData> + <!-- Create bundle product --> + <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink"> + <field key="price_type">0</field> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <field key="price_type">0</field> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Navigate to catalog page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$$createBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Add created product to Wishlist according to dataset and assert add product to wishlist success message --> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$createBundleProduct$$"/> + </actionGroup> + + <!-- Navigate to My Account > My Wishlist --> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishListPage"/> + <waitForPageLoad stepKey="waitForWishlistPageLoad"/> + + <!-- Click "Remove item" --> + <scrollTo selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName($$createBundleProduct.name$$)}}" stepKey="scrollToProduct"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName($$createBundleProduct.name$$)}}" stepKey="mouseOverOnProduct"/> + <click selector="{{StorefrontCustomerWishlistProductSection.removeProduct}}" stepKey="clickRemoveButton"/> + + <!-- Assert Wishlist is empty --> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmpty" stepKey="assertWishlistIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml new file mode 100644 index 0000000000000..ee66825878728 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml @@ -0,0 +1,150 @@ +<?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="StorefrontDeleteConfigurableProductFromWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Delete Configurable Product from Wishlist on Frontend"/> + <description value="Delete Configurable Product from Wishlist on Frontend"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14217"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the third option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">10.00</field> + </createData> + + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <field key="price">20.00</field> + </createData> + + <!--Create a simple product and give it the attribute with the Third option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <field key="price">30.00</field> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Add the third simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- 1. Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- 2. Navigate to catalog page --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + + <!-- 3. Add created product to Wishlist according to dataset and assert add product to wishlist success message --> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$createConfigProduct$$"/> + </actionGroup> + + <!-- Navigate to My Account > My Wishlist --> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishListPage"/> + <waitForPageLoad stepKey="waitForWishlistPageLoad"/> + + <!-- Click "Remove item" --> + <scrollTo selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName($$createConfigProduct.name$$)}}" stepKey="scrollToProduct"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.ProductInfoByName($$createConfigProduct.name$$)}}" stepKey="mouseOverOnProduct"/> + <click selector="{{StorefrontCustomerWishlistProductSection.removeProduct}}" stepKey="clickRemoveButton"/> + + <!-- Assert Wishlist is empty --> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmpty" stepKey="assertWishlistIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml index 3c84562542adf..f1659baaa4e09 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml @@ -32,7 +32,7 @@ <deleteData stepKey="deleteCategory" createDataKey="category"/> <deleteData stepKey="deleteProduct" createDataKey="product"/> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml new file mode 100644 index 0000000000000..70268f4e14e8f --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml @@ -0,0 +1,166 @@ +<?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="StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Move Configurable Product from Shopping Cart to Wishlist"/> + <description value="Move Configurable Product from Shopping Cart to Wishlist"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14211"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create an attribute with three options to be used in the first child product --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the third option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">10.00</field> + </createData> + + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <field key="price">20.00</field> + </createData> + + <!--Create a simple product and give it the attribute with the Third option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <field key="price">30.00</field> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Add the third simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- 1. Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Open Product page --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="selectOption1"/> + <scrollTo selector="{{StorefrontProductInfoMainSection.productAddToWishlist}}" y="-200" stepKey="scroll"/> + + <!-- Add product to the cart and Assert add product to cart success message--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartVirtualProductFromStorefrontProductPage"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + + <!-- Select Mini Cart and select 'View And Edit Cart' --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> + + <!-- Assert move product to wishlist success message --> + <actionGroup ref="AssertMoveProductToWishListSuccessMessage" stepKey="moveToWishlist"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + + <!-- Assert product is present in wishlist --> + <actionGroup ref="AssertProductIsPresentInWishList" stepKey="assertProductPresent"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + <argument name="productPrice" value="$20.00"/> + </actionGroup> + + <!-- Assert product details in Wishlist --> + <actionGroup ref="AssertProductDetailsInWishlist" stepKey="assertProductDetails"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + <argument name="label" value="$$createConfigProductAttribute.default_value$$"/> + <argument name="labelValue" value="$$getConfigAttributeOption2.label$$"/> + </actionGroup> + + <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="assertCartIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveDynamicBundleProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveDynamicBundleProductFromShoppingCartToWishlistTest.xml new file mode 100644 index 0000000000000..6faf4c1002c1a --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveDynamicBundleProductFromShoppingCartToWishlistTest.xml @@ -0,0 +1,109 @@ +<?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="StorefrontMoveDynamicBundleProductFromShoppingCartToWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Move Dynamic Bundle Product from Shopping Cart to Wishlist"/> + <description value="Move Dynamic Bundle Product from Shopping Cart to Wishlist"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14212"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">20.00</field> + </createData> + <!--Create Bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="goToProductPageViaID" stepKey="goToProduct"> + <argument name="productId" value="$$createBundleProduct.id$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductFormBundleSection.contentDropDown}}" stepKey="scrollToBundleSection"/> + <selectOption userInput="Separately" selector="{{AdminProductFormBundleSection.shipmentType}}" stepKey="selectSeparately"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- 1. Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Open Product page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$$createBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickCustomizeButton"/> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts($$createBundleOption1_1.title$$)}}" userInput="$$simpleProduct1.sku$$ +$100.00" stepKey="selectOption0Product0"/> + <fillField selector="{{StorefrontBundledSection.dropDownOptionOneQuantity($$createBundleOption1_1.title$$)}}" userInput="1" stepKey="fillQuantity00"/> + + <!-- Add product to the cart and Assert add product to cart success message--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartVirtualProductFromStorefrontProductPage"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + </actionGroup> + + <!-- Select Mini Cart and select 'View And Edit Cart' --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> + + <!-- Assert move product to wishlist success message --> + <actionGroup ref="AssertMoveProductToWishListSuccessMessage" stepKey="moveToWishlist"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + </actionGroup> + + <!-- Assert product is present in wishlist --> + <actionGroup ref="AssertProductIsPresentInWishList" stepKey="assertProductPresent"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="productPrice" value="$100.00"/> + </actionGroup> + + <!-- Assert product details in Wishlist --> + <actionGroup ref="AssertProductDetailsInWishlist" stepKey="assertProductDetails"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="label" value="$$createBundleOption1_1.title$$"/> + <argument name="labelValue" value="$$simpleProduct1.sku$$ $100.00"/> + </actionGroup> + + <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="assertCartIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml new file mode 100644 index 0000000000000..3906767d04cc1 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest.xml @@ -0,0 +1,100 @@ +<?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="StorefrontMoveFixedBundleProductFromShoppingCartToWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Move Fixed Bundle Product from Shopping Cart to Wishlist"/> + <description value="Move Fixed Bundle Product from Shopping Cart to Wishlist"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14213"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <!-- Create bundle product --> + <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink"> + <field key="price_type">0</field> + <field key="price">100</field> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <field key="price_type">0</field> + <field key="price">100</field> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- 1. Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Open Product page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$$createBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickCustomizeButton"/> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts($$createBundleOption1_1.title$$)}}" userInput="$$simpleProduct1.sku$$ +$100.00" stepKey="selectOption0Product0"/> + <fillField selector="{{StorefrontBundledSection.dropDownOptionOneQuantity($$createBundleOption1_1.title$$)}}" userInput="1" stepKey="fillQuantity00"/> + + <!-- Add product to the cart and Assert add product to cart success message--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartVirtualProductFromStorefrontProductPage"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + </actionGroup> + + <!-- Select Mini Cart and select 'View And Edit Cart' --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> + + <!-- Assert move product to wishlist success message --> + <actionGroup ref="AssertMoveProductToWishListSuccessMessage" stepKey="moveToWishlist"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + </actionGroup> + + <!-- Assert product is present in wishlist --> + <actionGroup ref="AssertProductIsPresentInWishList" stepKey="assertProductPresent"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="productPrice" value="$101.23"/> + </actionGroup> + + <!-- Assert product details in Wishlist --> + <actionGroup ref="AssertProductDetailsInWishlist" stepKey="assertProductDetails"> + <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="label" value="$$createBundleOption1_1.title$$"/> + <argument name="labelValue" value="$$simpleProduct1.sku$$ $100.00"/> + </actionGroup> + + <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="assertCartIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveVirtualProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveVirtualProductFromShoppingCartToWishlistTest.xml new file mode 100644 index 0000000000000..dc1580797e5be --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveVirtualProductFromShoppingCartToWishlistTest.xml @@ -0,0 +1,69 @@ +<?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="StorefrontMoveVirtualProductFromShoppingCartToWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Move Virtual Product from Shopping Cart to Wishlist"/> + <description value="Move Virtual Product from Shopping Cart to Wishlist"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14210"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="defaultVirtualProduct" stepKey="createProduct"> + <field key="price">40</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- 1. Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Open Virtual Product page --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="OpenStoreFrontProductPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!-- Add Virtual product to the cart and Assert add product to cart success message--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartVirtualProductFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Select Mini Cart and select 'View And Edit Cart' --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="selectViewAndEditCart"/> + + <!-- Assert move product to wishlist success message --> + <actionGroup ref="AssertMoveProductToWishListSuccessMessage" stepKey="moveToWishlist"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Assert product is present in wishlist --> + <actionGroup ref="AssertProductIsPresentInWishList" stepKey="assertProductPresent"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productPrice" value="$$createProduct.price$$"/> + </actionGroup> + + <!-- Assert cart is empty --> + <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="assertCartIsEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml index e3382dc41d27e..6c73cb6708ae4 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml @@ -34,6 +34,11 @@ <deleteData createDataKey="categorySecond" stepKey="deleteCategorySecond"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index e482449f623fc..b8a84a327b58f 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -26,6 +26,10 @@ <createData entity="Simple_US_Customer" stepKey="customer"/> </before> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> </actionGroup> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php index e9061f1f3d5f8..c1f1378c22da6 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php @@ -172,7 +172,7 @@ protected function setUp() $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->disableOriginalConstructor() - ->setMethods(['addSuccess']) + ->setMethods(['addSuccessMessage']) ->getMockForAbstractClass(); $this->urlMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) @@ -566,7 +566,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->willReturn($productName); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('You added ' . $productName . ' to your shopping cart.', null) ->willReturnSelf(); @@ -581,7 +581,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) $this->helperMock->expects($this->once()) ->method('calculate') ->willReturnSelf(); - + return $refererUrl; } diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/PluginTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/PluginTest.php index 399b48073b339..2b583f9101516 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/PluginTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/PluginTest.php @@ -6,6 +6,9 @@ namespace Magento\Wishlist\Test\Unit\Controller\Index; +/** + * Test for wishlist plugin before dispatch + */ class PluginTest extends \PHPUnit\Framework\TestCase { /** @@ -38,22 +41,26 @@ class PluginTest extends \PHPUnit\Framework\TestCase */ protected $request; + /** + * @inheritdoc + */ protected function setUp() { $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() - ->setMethods([ - 'authenticate', - 'getBeforeWishlistUrl', - 'setBeforeWishlistUrl', - 'setBeforeWishlistRequest', - 'getBeforeWishlistRequest', - 'setBeforeRequestParams', - 'setBeforeModuleName', - 'setBeforeControllerName', - 'setBeforeAction', - ]) - ->getMock(); + ->setMethods( + [ + 'authenticate', + 'getBeforeWishlistUrl', + 'setBeforeWishlistUrl', + 'setBeforeWishlistRequest', + 'getBeforeWishlistRequest', + 'setBeforeRequestParams', + 'setBeforeModuleName', + 'setBeforeControllerName', + 'setBeforeAction', + ] + )->getMock(); $this->authenticationState = $this->createMock(\Magento\Wishlist\Model\AuthenticationState::class); $this->config = $this->createMock(\Magento\Framework\App\Config::class); @@ -62,6 +69,9 @@ protected function setUp() $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); } + /** + * @inheritdoc + */ protected function tearDown() { unset( @@ -96,6 +106,7 @@ public function testBeforeDispatch() $refererUrl = 'http://referer-url.com'; $params = [ 'product' => 1, + 'login' => [], ]; $actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); @@ -139,7 +150,7 @@ public function testBeforeDispatch() ->willReturnSelf(); $this->customerSession->expects($this->once()) ->method('setBeforeWishlistRequest') - ->with($params) + ->with(['product' => 1]) ->willReturnSelf(); $this->customerSession->expects($this->once()) ->method('getBeforeWishlistRequest') diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/RemoveTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/RemoveTest.php index bb4ae44fcc31d..2f4d0e6ba48ab 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/RemoveTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/RemoveTest.php @@ -244,7 +244,7 @@ public function testExecuteWithoutWishlist() ->method('create') ->with(\Magento\Wishlist\Model\Item::class) ->willReturn($item); - + $this->wishlistProvider ->expects($this->once()) ->method('getWishlist') @@ -273,7 +273,7 @@ public function testExecuteCanNotSaveWishlist() $this->messageManager ->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('We can\'t delete the item from Wish List right now because of an error: Message.') ->willReturn(true); @@ -356,7 +356,7 @@ public function testExecuteCanNotSaveWishlistAndWithRedirect() $this->messageManager ->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('We can\'t delete the item from the Wish List right now.') ->willReturn(true); diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/UpdateItemOptionsTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/UpdateItemOptionsTest.php index 0c2d7765b1ff2..b6fd509214897 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/UpdateItemOptionsTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/UpdateItemOptionsTest.php @@ -249,7 +249,7 @@ public function testExecuteWithoutProduct() $this->messageManager ->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('We can\'t specify a product.') ->willReturn(true); $this->resultRedirectMock->expects($this->once()) @@ -294,7 +294,7 @@ public function testExecuteWithoutWishList() $this->messageManager ->expects($this->never()) - ->method('addError') + ->method('addErrorMessage') ->with('We can\'t specify a product.') ->willReturn(true); @@ -433,12 +433,12 @@ public function testExecuteAddSuccessException() $this->messageManager ->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('Test name has been updated in your Wish List.', null) ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('error-message'))); $this->messageManager ->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('error-message', null) ->willReturn(true); $this->resultRedirectMock->expects($this->once()) @@ -572,12 +572,12 @@ public function testExecuteAddSuccessCriticalException() $this->messageManager ->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('Test name has been updated in your Wish List.', null) ->willThrowException($exception); $this->messageManager ->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('We can\'t update your Wish List right now.', null) ->willReturn(true); $this->resultRedirectMock->expects($this->once()) diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php index 118c27ae3eee2..c65f166957c5f 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php @@ -266,7 +266,7 @@ public function testExecute( $successMessage = __('You added %1 to your shopping cart.', $productName); $this->messageManager->expects($this->any()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with($successMessage) ->willReturnSelf(); diff --git a/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php b/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php index 1769306172aab..263f3c5d1688e 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Helper/DataTest.php @@ -18,6 +18,7 @@ use Magento\Wishlist\Controller\WishlistProviderInterface; use Magento\Wishlist\Model\Item as WishlistItem; use Magento\Wishlist\Model\Wishlist; +use Magento\Customer\Model\Session; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -63,6 +64,9 @@ class DataTest extends \PHPUnit\Framework\TestCase /** @var Context |\PHPUnit_Framework_MockObject_MockObject */ protected $context; + /** @var Session |\PHPUnit_Framework_MockObject_MockObject */ + protected $customerSession; + /** * Set up mock objects for tested class * @@ -121,12 +125,13 @@ protected function setUp() $this->wishlistItem = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) ->disableOriginalConstructor() - ->setMethods([ - 'getProduct', - 'getWishlistItemId', - 'getQty', - ]) - ->getMock(); + ->setMethods( + [ + 'getProduct', + 'getWishlistItemId', + 'getQty', + ] + )->getMock(); $this->wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) ->disableOriginalConstructor() @@ -136,11 +141,16 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( \Magento\Wishlist\Helper\Data::class, [ 'context' => $this->context, + 'customerSession' => $this->customerSession, 'storeManager' => $this->storeManager, 'wishlistProvider' => $this->wishlistProvider, 'coreRegistry' => $this->coreRegistry, @@ -431,4 +441,20 @@ public function testGetSharedAddAllToCartUrl() $this->assertEquals($url, $this->model->getSharedAddAllToCartUrl()); } + + public function testGetRssUrlWithCustomerNotLogin() + { + $url = 'result url'; + + $this->customerSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->urlBuilder->expects($this->once()) + ->method('getUrl') + ->with('wishlist/index/rss', []) + ->willReturn($url); + + $this->assertEquals($url, $this->model->getRssUrl()); + } } diff --git a/app/code/Magento/Wishlist/Test/Unit/Helper/RssTest.php b/app/code/Magento/Wishlist/Test/Unit/Helper/RssTest.php index b55766d77fee3..d0397be83fac7 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Helper/RssTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Helper/RssTest.php @@ -46,7 +46,7 @@ class RssTest extends \PHPUnit\Framework\TestCase protected $customerRepositoryMock; /** - * @var \Magento\Framework\Module\ModuleManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Module\Manager|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleManagerMock; @@ -80,7 +80,7 @@ protected function setUp() $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMock(); - $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleManagerInterface::class) + $this->moduleManagerMock = $this->getMockBuilder(\Magento\Framework\Module\Manager::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/ItemCarrierTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/ItemCarrierTest.php index 43f77e00bdf98..71ae2d182d0e4 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/ItemCarrierTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/ItemCarrierTest.php @@ -228,7 +228,7 @@ public function testMoveAllToCart() ->willReturn($productTwoName); $this->managerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('%1 product(s) have been added to shopping cart: %2.', 1, '"' . $productTwoName . '"'), null) ->willReturnSelf(); @@ -431,12 +431,12 @@ public function testMoveAllToCartWithNotSalableAndOptions() ->willReturn($productTwoName); $this->managerMock->expects($this->at(0)) - ->method('addError') + ->method('addErrorMessage') ->with(__('%1 for "%2".', 'Localized Exception', $productTwoName), null) ->willReturnSelf(); $this->managerMock->expects($this->at(1)) - ->method('addError') + ->method('addErrorMessage') ->with( __( 'We couldn\'t add the following product(s) to the shopping cart: %1.', @@ -580,7 +580,7 @@ public function testMoveAllToCartWithException() ->with($exception, []); $this->managerMock->expects($this->at(0)) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t add this item to your shopping cart right now.'), null) ->willReturnSelf(); @@ -603,7 +603,7 @@ public function testMoveAllToCartWithException() ->willThrowException(new \Exception()); $this->managerMock->expects($this->at(1)) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t update the Wish List right now.'), null) ->willReturnSelf(); @@ -615,7 +615,7 @@ public function testMoveAllToCartWithException() ->willReturn($productTwoName); $this->managerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('%1 product(s) have been added to shopping cart: %2.', 1, '"' . $productOneName . '"'), null) ->willReturnSelf(); diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php index ff8a3a3b87cec..eb788efc0d622 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php @@ -5,76 +5,104 @@ */ namespace Magento\Wishlist\Test\Unit\Model; +use ArrayIterator; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Catalog\Model\ProductFactory; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Stock\Item as StockItem; +use Magento\CatalogInventory\Model\Stock\StockItemRepository; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Math\Random; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Wishlist\Helper\Data; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\ItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResource; +use Magento\Wishlist\Model\ResourceModel\Wishlist\Collection as WishlistCollection; use Magento\Wishlist\Model\Wishlist; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class WishlistTest extends \PHPUnit\Framework\TestCase +class WishlistTest extends TestCase { /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + * @var Registry|PHPUnit_Framework_MockObject_MockObject */ protected $registry; /** - * @var \Magento\Catalog\Helper\Product|\PHPUnit_Framework_MockObject_MockObject + * @var HelperProduct|PHPUnit_Framework_MockObject_MockObject */ protected $productHelper; /** - * @var \Magento\Wishlist\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var Data|PHPUnit_Framework_MockObject_MockObject */ protected $helper; /** - * @var \Magento\Wishlist\Model\ResourceModel\Wishlist|\PHPUnit_Framework_MockObject_MockObject + * @var WishlistResource|PHPUnit_Framework_MockObject_MockObject */ protected $resource; /** - * @var \Magento\Wishlist\Model\ResourceModel\Wishlist\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var WishlistCollection|PHPUnit_Framework_MockObject_MockObject */ protected $collection; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|PHPUnit_Framework_MockObject_MockObject */ protected $storeManager; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var DateTime\DateTime|PHPUnit_Framework_MockObject_MockObject */ protected $date; /** - * @var \Magento\Wishlist\Model\ItemFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ItemFactory|PHPUnit_Framework_MockObject_MockObject */ protected $itemFactory; /** - * @var \Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject */ protected $itemsFactory; /** - * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ProductFactory|PHPUnit_Framework_MockObject_MockObject */ protected $productFactory; /** - * @var \Magento\Framework\Math\Random|\PHPUnit_Framework_MockObject_MockObject + * @var Random|PHPUnit_Framework_MockObject_MockObject */ protected $mathRandom; /** - * @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var DateTime|PHPUnit_Framework_MockObject_MockObject */ protected $dateTime; /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ManagerInterface|PHPUnit_Framework_MockObject_MockObject */ protected $eventDispatcher; @@ -84,63 +112,79 @@ class WishlistTest extends \PHPUnit\Framework\TestCase protected $wishlist; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ProductRepositoryInterface|PHPUnit_Framework_MockObject_MockObject */ protected $productRepository; /** - * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|PHPUnit_Framework_MockObject_MockObject */ protected $serializer; + /** + * @var StockItemRepository|PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var StockRegistryInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $stockRegistry; + protected function setUp() { - $context = $this->getMockBuilder(\Magento\Framework\Model\Context::class) + $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); - $this->eventDispatcher = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + $this->eventDispatcher = $this->getMockBuilder(ManagerInterface::class) ->getMock(); - $this->registry = $this->getMockBuilder(\Magento\Framework\Registry::class) + $this->registry = $this->getMockBuilder(Registry::class) ->disableOriginalConstructor() ->getMock(); - $this->productHelper = $this->getMockBuilder(\Magento\Catalog\Helper\Product::class) + $this->productHelper = $this->getMockBuilder(HelperProduct::class) ->disableOriginalConstructor() ->getMock(); - $this->helper = $this->getMockBuilder(\Magento\Wishlist\Helper\Data::class) + $this->helper = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); - $this->resource = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Wishlist::class) + $this->resource = $this->getMockBuilder(WishlistResource::class) ->disableOriginalConstructor() ->getMock(); - $this->collection = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Wishlist\Collection::class) + $this->collection = $this->getMockBuilder(WishlistCollection::class) ->disableOriginalConstructor() ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) ->getMock(); - $this->date = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) + $this->date = $this->getMockBuilder(DateTime\DateTime::class) ->disableOriginalConstructor() ->getMock(); - $this->itemFactory = $this->getMockBuilder(\Magento\Wishlist\Model\ItemFactory::class) + $this->itemFactory = $this->getMockBuilder(ItemFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->itemsFactory = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory::class) + $this->itemsFactory = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->productFactory = $this->getMockBuilder(\Magento\Catalog\Model\ProductFactory::class) + $this->productFactory = $this->getMockBuilder(ProductFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->mathRandom = $this->getMockBuilder(\Magento\Framework\Math\Random::class) + $this->mathRandom = $this->getMockBuilder(Random::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTime = $this->getMockBuilder(DateTime::class) ->disableOriginalConstructor() ->getMock(); - $this->dateTime = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime::class) + $this->productRepository = $this->createMock(ProductRepositoryInterface::class); + $this->stockRegistry = $this->createMock(StockRegistryInterface::class); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->productRepository = $this->createMock(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->serializer = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class) + $this->serializer = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); @@ -165,7 +209,9 @@ protected function setUp() $this->productRepository, false, [], - $this->serializer + $this->serializer, + $this->stockRegistry, + $this->scopeConfig ); } @@ -186,7 +232,7 @@ public function testLoadByCustomerId() ->will($this->returnValue($sharingCode)); $this->assertInstanceOf( - \Magento\Wishlist\Model\Wishlist::class, + Wishlist::class, $this->wishlist->loadByCustomerId($customerId, true) ); $this->assertEquals($customerId, $this->wishlist->getCustomerId()); @@ -194,10 +240,10 @@ public function testLoadByCustomerId() } /** - * @param int|\Magento\Wishlist\Model\Item|\PHPUnit_Framework_MockObject_MockObject $itemId - * @param \Magento\Framework\DataObject $buyRequest - * @param null|array|\Magento\Framework\DataObject $param - * @throws \Magento\Framework\Exception\LocalizedException + * @param int|Item|PHPUnit_Framework_MockObject_MockObject $itemId + * @param DataObject $buyRequest + * @param null|array|DataObject $param + * @throws LocalizedException * * @dataProvider updateItemDataProvider */ @@ -205,9 +251,9 @@ public function testUpdateItem($itemId, $buyRequest, $param) { $storeId = 1; $productId = 1; - $stores = [(new \Magento\Framework\DataObject())->setId($storeId)]; + $stores = [(new DataObject())->setId($storeId)]; - $newItem = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $newItem = $this->getMockBuilder(Item::class) ->setMethods( ['setProductId', 'setWishlistId', 'setStoreId', 'setOptions', 'setProduct', 'setQty', 'getItem', 'save'] ) @@ -228,26 +274,30 @@ public function testUpdateItem($itemId, $buyRequest, $param) $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($stores[0])); $product = $this->getMockBuilder( - \Magento\Catalog\Model\Product::class + Product::class )->disableOriginalConstructor()->getMock(); $product->expects($this->any())->method('getId')->will($this->returnValue($productId)); $product->expects($this->any())->method('getStoreId')->will($this->returnValue($storeId)); - $instanceType = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + $stockItem = $this->getMockBuilder(StockItem::class)->disableOriginalConstructor()->getMock(); + $stockItem->expects($this->any())->method('getIsInStock')->will($this->returnValue(true)); + $this->stockRegistry->expects($this->any()) + ->method('getStockItem') + ->will($this->returnValue($stockItem)); + + $instanceType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); $instanceType->expects($this->once()) ->method('processConfiguration') ->will( $this->returnValue( - $this->getMockBuilder( - \Magento\Catalog\Model\Product::class - )->disableOriginalConstructor()->getMock() + $this->getMockBuilder(Product::class)->disableOriginalConstructor()->getMock() ) ); $newProduct = $this->getMockBuilder( - \Magento\Catalog\Model\Product::class + Product::class )->disableOriginalConstructor()->getMock(); $newProduct->expects($this->any()) ->method('setStoreId') @@ -257,12 +307,12 @@ public function testUpdateItem($itemId, $buyRequest, $param) ->method('getTypeInstance') ->will($this->returnValue($instanceType)); - $item = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class)->disableOriginalConstructor()->getMock(); + $item = $this->getMockBuilder(Item::class)->disableOriginalConstructor()->getMock(); $item->expects($this->once()) ->method('getProduct') ->will($this->returnValue($product)); - $items = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\Collection::class) + $items = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -280,7 +330,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) ->will($this->returnValue($item)); $items->expects($this->any()) ->method('getIterator') - ->will($this->returnValue(new \ArrayIterator([$item]))); + ->will($this->returnValue(new ArrayIterator([$item]))); $this->itemsFactory->expects($this->any()) ->method('create') @@ -292,7 +342,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) ->will($this->returnValue($newProduct)); $this->assertInstanceOf( - \Magento\Wishlist\Model\Wishlist::class, + Wishlist::class, $this->wishlist->updateItem($itemId, $buyRequest, $param) ); } @@ -303,7 +353,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) public function updateItemDataProvider() { return [ - '0' => [1, new \Magento\Framework\DataObject(), null] + '0' => [1, new DataObject(), null] ]; } @@ -311,24 +361,26 @@ public function testAddNewItem() { $productId = 1; $storeId = 1; - $buyRequest = json_encode([ - 'number' => 42, - 'string' => 'string_value', - 'boolean' => true, - 'collection' => [1, 2, 3], - 'product' => 1, - 'form_key' => 'abc' - ]); + $buyRequest = json_encode( + [ + 'number' => 42, + 'string' => 'string_value', + 'boolean' => true, + 'collection' => [1, 2, 3], + 'product' => 1, + 'form_key' => 'abc' + ] + ); $result = 'product'; - $instanceType = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + $instanceType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); $instanceType->expects($this->once()) ->method('processConfiguration') ->willReturn('product'); - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + $productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getId', 'hasWishlistStoreId', 'getStoreId', 'getTypeInstance']) ->getMock(); @@ -358,6 +410,15 @@ function ($value) { } ); + $stockItem = $this->getMockBuilder( + StockItem::class + )->disableOriginalConstructor()->getMock(); + $stockItem->expects($this->any())->method('getIsInStock')->will($this->returnValue(true)); + + $this->stockRegistry->expects($this->any()) + ->method('getStockItem') + ->will($this->returnValue($stockItem)); + $this->assertEquals($result, $this->wishlist->addNewItem($productMock, $buyRequest)); } } diff --git a/app/code/Magento/Wishlist/Test/Unit/ViewModel/AllowedQuantityTest.php b/app/code/Magento/Wishlist/Test/Unit/ViewModel/AllowedQuantityTest.php new file mode 100644 index 0000000000000..23e28abf20959 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/ViewModel/AllowedQuantityTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\ViewModel; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Store\Model\Store; +use Magento\Wishlist\ViewModel\AllowedQuantity; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class AllowedQuantityTest + */ +class AllowedQuantityTest extends TestCase +{ + /** + * @var AllowedQuantity + */ + private $sut; + + /** + * @var StockRegistry|MockObject + */ + private $stockRegistryMock; + + /** + * @var ItemInterface|MockObject + */ + private $itemMock; + + /** + * @var Product|MockObject + */ + private $productMock; + + /** + * @var Store|MockObject + */ + private $storeMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->stockRegistryMock = $this->createMock(StockRegistry::class); + $this->itemMock = $this->getMockForAbstractClass( + ItemInterface::class, + [], + '', + false, + true, + true, + ['getMinSaleQty', 'getMaxSaleQty'] + ); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sut = new AllowedQuantity( + $this->stockRegistryMock + ); + $this->sut->setItem($this->itemMock); + } + + /** + * Getting min and max qty test. + * + * @dataProvider saleQuantityDataProvider + * + * @param int $minSaleQty + * @param int $maxSaleQty + * @param array $expectedResult + */ + public function testGettingMinMaxQty(int $minSaleQty, int $maxSaleQty, array $expectedResult) + { + $this->storeMock->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn(1); + $this->productMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $this->productMock->expects($this->atLeastOnce()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->itemMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->itemMock->expects($this->any()) + ->method('getMinSaleQty') + ->willReturn($minSaleQty); + $this->itemMock->expects($this->any()) + ->method('getMaxSaleQty') + ->willReturn($maxSaleQty); + $this->stockRegistryMock->expects($this->any()) + ->method('getStockItem') + ->will($this->returnValue($this->itemMock)); + + $result = $this->sut->getMinMaxQty(); + + $this->assertEquals($result, $expectedResult); + } + + /** + * Sales quantity provider + * + * @return array + */ + public function saleQuantityDataProvider(): array + { + return [ + [ + 1, + 10, + [ + 'minAllowed' => 1, + 'maxAllowed' => 10 + ] + ], [ + 1, + 0, + [ + 'minAllowed' => 1, + 'maxAllowed' => 99999999 + ] + ] + ]; + } +} diff --git a/app/code/Magento/Wishlist/etc/db_schema.xml b/app/code/Magento/Wishlist/etc/db_schema.xml index 8a02f411ad0db..e3f3024df45fd 100644 --- a/app/code/Magento/Wishlist/etc/db_schema.xml +++ b/app/code/Magento/Wishlist/etc/db_schema.xml @@ -64,11 +64,11 @@ </table> <table name="wishlist_item_option" resource="default" engine="innodb" comment="Wishlist Item Option Table"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="wishlist_item_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Wishlist Item Id"/> + comment="Wishlist Item ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Wishlist/view/frontend/email/share_notification.html b/app/code/Magento/Wishlist/view/frontend/email/share_notification.html index 31ea2f72346ea..d5c31ab858de8 100644 --- a/app/code/Magento/Wishlist/view/frontend/email/share_notification.html +++ b/app/code/Magento/Wishlist/view/frontend/email/share_notification.html @@ -6,6 +6,7 @@ --> <!--@subject {{trans "Take a look at %customer_name's Wish List" customer_name=$customerName}} @--> <!--@vars { +"var store.frontend_name":"Store Name", "var customerName":"Customer Name", "var viewOnSiteLink":"View Wish List URL", "var items|raw":"Wish List Items", @@ -14,7 +15,7 @@ {{template config_path="design/email/header_template"}} -<p>{{trans "%customer_name wants to share this Wish List from %store_name with you:" customer_name=$customerName store_name=$store.getFrontendName()}}</p> +<p>{{trans "%customer_name wants to share this Wish List from %store_name with you:" customer_name=$customerName store_name=$store.frontend_name}}</p> {{depend message}} <table class="message-info"> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 033e2e43a3c22..aca843872af65 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -79,7 +79,9 @@ define([ $(element).is('textarea') || $('#' + element.id + ' option:selected').length ) { - dataToAdd = $.extend({}, dataToAdd, self._getElementData(element)); + if ($(element).data('selector') || $(element).attr('name')) { + dataToAdd = $.extend({}, dataToAdd, self._getElementData(element)); + } return; } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php new file mode 100644 index 0000000000000..e866b9cead03c --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; + +/** + * Fetches customer wishlist data + */ +class CustomerWishlistResolver implements ResolverInterface +{ + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistFactory $wishlistFactory + */ + public function __construct(WishlistFactory $wishlistFactory) + { + $this->wishlistFactory = $wishlistFactory; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($context->getUserId(), true); + return [ + 'id' => (string) $wishlist->getId(), + 'sharing_code' => $wishlist->getSharingCode(), + 'updated_at' => $wishlist->getUpdatedAt(), + 'items_count' => $wishlist->getItemsCount(), + 'model' => $wishlist, + ]; + } +} diff --git a/app/code/Magento/WishlistGraphQl/etc/module.xml b/app/code/Magento/WishlistGraphQl/etc/module.xml index 337623cc85a92..c2f5b3165b2ab 100644 --- a/app/code/Magento/WishlistGraphQl/etc/module.xml +++ b/app/code/Magento/WishlistGraphQl/etc/module.xml @@ -6,5 +6,10 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_WishlistGraphQl" /> + <module name="Magento_WishlistGraphQl"> + <sequence> + <module name="Magento_Customer"/> + <module name="Magento_CustomerGraphQl"/> + </sequence> + </module> </config> diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 2aa5f03a787d0..deaa66921ba7c 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -2,13 +2,25 @@ # See COPYING.txt for license details. type Query { - wishlist: WishlistOutput @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish list") @cache(cacheable: false) + wishlist: WishlistOutput @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistResolver") @deprecated(reason: "Moved under `Customer` `wishlist`") @doc(description: "The wishlist query returns the contents of a customer's wish list") @cache(cacheable: false) } -type WishlistOutput { +type Customer { + wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish lists") @cache(cacheable: false) +} + +type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { + items: [WishlistItem] @deprecated(reason: "Use field `items` from type `Wishlist` instead") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItemsResolver") @doc(description: "An array of items in the customer's wish list"), + items_count: Int @deprecated(reason: "Use field `items_count` from type `Wishlist` instead") @doc(description: "The number of items in the wish list"), + name: String @deprecated(reason: "This field is related to Commerce functionality and is always `null` in Open Source edition") @doc(description: "When multiple wish lists are enabled, the name the customer assigns to the wishlist"), + sharing_code: String @deprecated(reason: "Use field `sharing_code` from type `Wishlist` instead") @doc(description: "An encrypted code that Magento uses to link to the wish list"), + updated_at: String @deprecated(reason: "Use field `updated_at` from type `Wishlist` instead") @doc(description: "The time of the last modification to the wish list") +} + +type Wishlist { + id: ID @doc(description: "Wishlist unique identifier") items: [WishlistItem] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItemsResolver") @doc(description: "An array of items in the customer's wish list"), items_count: Int @doc(description: "The number of items in the wish list"), - name: String @doc(description: "When multiple wish lists are enabled, the name the customer assigns to the wishlist"), sharing_code: String @doc(description: "An encrypted code that Magento uses to link to the wish list"), updated_at: String @doc(description: "The time of the last modification to the wish list") } @@ -19,4 +31,4 @@ type WishlistItem { description: String @doc(description: "The customer's comment about this item"), added_at: String @doc(description: "The time when the customer added the item to the wish list"), product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") -} \ No newline at end of file +} diff --git a/app/design/adminhtml/Magento/backend/Magento_AdminAnalytics/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_AdminAnalytics/web/css/source/_module.less new file mode 100644 index 0000000000000..05c0653a9bac3 --- /dev/null +++ b/app/design/adminhtml/Magento/backend/Magento_AdminAnalytics/web/css/source/_module.less @@ -0,0 +1,43 @@ +// /** +// * Copyright © Magento, Inc. All rights reserved. +// * See COPYING.txt for license details. +// */ + +// +// Magento_AdminAnalytics Modal on dashboard +// --------------------------------------------- + +.admin-usage-notification { + -webkit-transition: visibility 0s .5s, opacity .5s ease; + transition: visibility 0s .5s, opacity .5s ease; + + &._show { + -webkit-transition: opacity .5s ease; + opacity: 1; + transition: opacity .5s ease; + visibility: visible; + } + + .modal-inner-wrap { + .modal-content, + .modal-header { + padding-left: 4rem; + padding-right: 4rem; + + .action-close { + display: none; + } + } + + -webkit-transform: translateX(0); + -webkit-transition: -webkit-transform 0s; + transition: transform 0s; + transform: translateX(0); + margin-top: 13rem; + max-width: 75rem; + } + + .admin__fieldset { + padding: 0; + } +} diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less index 40ebb6f3c4569..6b30bf70772a4 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less @@ -97,13 +97,14 @@ display: inline-block; font-size: @notifications__font-size; font-weight: @font-weight__bold; - height: 18px; + height: 20px; left: 50%; + line-height: 20px; margin-left: .3em; margin-top: -1.1em; - min-width: 18px; - padding: .3em .5em; + min-width: 20px; position: absolute; + text-align: center; top: 50%; } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less index d9e2cfdd66bf7..dd67220db12da 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less @@ -24,6 +24,7 @@ input[type='checkbox'].banner-content-checkbox { } .adminhtml-widget_instance-edit, +.adminhtml-cms_page-edit, .adminhtml-banner-edit { .admin__fieldset { .admin__field-control { diff --git a/app/design/adminhtml/Magento/backend/Magento_Cms/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Cms/web/css/source/_module.less new file mode 100644 index 0000000000000..715ad40dc475f --- /dev/null +++ b/app/design/adminhtml/Magento/backend/Magento_Cms/web/css/source/_module.less @@ -0,0 +1,22 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +.modal-slide { + .media-gallery-modal { + .page-main-actions { + margin-bottom: 3rem; + } + + .new_folder { + margin-right: 10px; + } + } +} + +.tree-actions { + a { + cursor: pointer; + } +} + diff --git a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less index 659b1fa811db1..fa158589feb96 100644 --- a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less +++ b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less @@ -22,10 +22,10 @@ position: relative; display: -webkit-inline-flex; display: -ms-inline-flexbox; + display: inline-flex; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; - display: inline-flex; flex-flow: row nowrap; width: 100%; diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less index ffa5ee963952c..055e74c97a2f4 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less @@ -308,4 +308,62 @@ } } +// +// Create Order - Add Product Grid +// --------------------------------------------- + +#sales_order_create_search_grid { + .col-in_products { + .data-grid-checkbox-cell-inner { + position: relative; + } + .checkbox { + width: 1.6rem; + height: 1.6rem; + left: 0; + right: 0; + margin: auto; + } + } +} + +// +// Create Order - Add Product with Custom Options Modal +// ---------------------------------------------------- + +#product_composite_configure_form_fields { + .admin__field { + &.required { + .admin__field-label { + &:after { + color: #e22626; + content: '*'; + display: inline-block; + font-size: 1.6rem; + font-weight: 500; + line-height: 1; + margin-left: 10px; + margin-top: .2rem; + position: absolute; + z-index: 1; + } + } + .price-container, .price-notice, .price-wrapper { + &:after { + color: unset; + content: unset; + display: unset; + font-size: unset; + font-weight: unset; + line-height: unset; + margin-left: unset; + margin-top: unset; + position: unset; + z-index: unset; + } + } + } + } +} + // ToDo: MAGETWO-32299 UI: review the collapsible block diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less index 029594625ed1c..2c55d243ebe07 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less @@ -73,7 +73,7 @@ } .order-shipping-address & { span { - top: 22px; + top: 0; } } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/_module.less index e05e81d737f14..9255accff6950 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/_module.less @@ -4,3 +4,4 @@ // */ @import 'module/_data-grid.less'; +@import 'module/_masonry-grid.less'; diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less index 946d11db2d1a2..b0c44e127a454 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less @@ -1075,7 +1075,6 @@ body._in-resize { } .data-grid-checkbox-cell-inner { - display: unset; margin: 0 @data-grid-checkbox-cell-inner__padding-horizontal 0; padding: 0; text-align: center; diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_masonry-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_masonry-grid.less new file mode 100644 index 0000000000000..c17c19e259478 --- /dev/null +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_masonry-grid.less @@ -0,0 +1,108 @@ +// /** +// * Copyright © Magento, Inc. All rights reserved. +// * See COPYING.txt for license details. +// */ +@admin__masonry_grid_image__space: 20px; +@admin__masonry_grid_background_color: #fff; +@admin__masonry_overlay_background_color: #507dc8; + +& when (@media-common = true) { + .masonry-image { + &-grid { + margin: @admin__masonry_grid_image__space/2 -(@admin__masonry_grid_image__space/2); + overflow: hidden; + position: relative; + + .no-data-message-container, + .error-message-container { + font-size: @data-grid__no-records__font-size; + padding: @data-grid__no-records__padding; + text-align: center; + } + } + + &-column { + background-color: @admin__masonry_grid_background_color; + float: left; + margin: @admin__masonry_grid_image__space/2; + overflow: hidden; + + img { + cursor: pointer; + height: 100%; + width: 100%; + } + } + + &-overlay { + background-color: @admin__masonry_overlay_background_color; + color: @admin__masonry_grid_background_color; + opacity: 1; + padding: .5rem; + position: absolute; + text-align: center; + width: 80px; + z-index: 10; + } + + &-preview { + background-color: @admin__masonry_grid_background_color; + display: table; + left: 0; + position: absolute; + right: 0; + width: 100%; + + .container { + margin: auto; + max-width: 880px; + padding-top: 10px; + + .action-buttons { + text-align: right; + + .action { + &-close { + padding: 30px; + position: static; + } + + &-previous, + &-next { + background: transparent; + border: none; + margin: 0; + white-space: nowrap; + } + + &-close, + &-previous, + &-next { + font-size: 2rem; + } + } + } + + .preview-row-content { + display: flex; + + &:after { + clear: both; + content: ''; + display: table; + } + + img.preview { + display: block; + flex-basis: 300px; + float: left; + margin-bottom: 20px; + max-height: 500px; + max-width: 60%; + width: auto; + } + } + } + } + } +} diff --git a/app/design/adminhtml/Magento/backend/etc/view.xml b/app/design/adminhtml/Magento/backend/etc/view.xml index 18c2d8f1b1722..621c18fc97cc8 100644 --- a/app/design/adminhtml/Magento/backend/etc/view.xml +++ b/app/design/adminhtml/Magento/backend/etc/view.xml @@ -24,7 +24,6 @@ </media> <exclude> <item type="file">Lib::mage/captcha.js</item> - <item type="file">Lib::mage/captcha.min.js</item> <item type="file">Lib::mage/common.js</item> <item type="file">Lib::mage/cookies.js</item> <item type="file">Lib::mage/dataPost.js</item> @@ -46,7 +45,6 @@ <item type="file">Lib::mage/translate-inline-vde.js</item> <item type="file">Lib::mage/webapi.js</item> <item type="file">Lib::mage/zoom.js</item> - <item type="file">Lib::mage/validation/dob-rule.js</item> <item type="file">Lib::mage/validation/validation.js</item> <item type="file">Lib::mage/adminhtml/varienLoader.js</item> <item type="file">Lib::mage/adminhtml/tools.js</item> @@ -57,12 +55,11 @@ <item type="file">Lib::jquery/jquery.parsequery.js</item> <item type="file">Lib::jquery/jquery.mobile.custom.js</item> <item type="file">Lib::jquery/jquery-ui.js</item> - <item type="file">Lib::jquery/jquery-ui.min.js</item> <item type="file">Lib::matchMedia.js</item> <item type="file">Lib::requirejs/require.js</item> <item type="file">Lib::requirejs/text.js</item> - <item type="file">Lib::date-format-normalizer.js</item> <item type="file">Lib::varien/js.js</item> + <item type="directory">Magento_Tinymce3::tiny_mce</item> <item type="directory">Lib::css</item> <item type="directory">Lib::lib</item> <item type="directory">Lib::prototype</item> @@ -72,10 +69,5 @@ <item type="directory">Lib::fotorama</item> <item type="directory">Lib::magnifier</item> <item type="directory">Lib::tiny_mce</item> - <item type="directory">Lib::tiny_mce/classes</item> - <item type="directory">Lib::tiny_mce/langs</item> - <item type="directory">Lib::tiny_mce/plugins</item> - <item type="directory">Lib::tiny_mce/themes</item> - <item type="directory">Lib::tiny_mce/utils</item> </exclude> </view> diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_media-gallery.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_media-gallery.less index 9a5f35e4ede90..6de25c424656d 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_media-gallery.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_media-gallery.less @@ -280,6 +280,7 @@ .item-role { background: @color-gray89; color: @color-brownie; + cursor: pointer; font-size: @font-size__s; line-height: 1; margin: 0 .4rem .4rem 0; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index 08aeb35d7adb2..bfb515c700b33 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -122,7 +122,7 @@ > .admin__field-control { #mix-grid .column(@field-control-grid__column, @field-grid__columns); input[type="checkbox"] { - margin-top: @indent__s; + margin-top: 0; } } @@ -156,7 +156,7 @@ .admin__field { margin-top: 8px; } - } + } } } &.composite-bundle { @@ -307,7 +307,7 @@ .admin__fieldset > & { margin-bottom: 3rem; position: relative; - + &.field-import_file { .input-file { margin-top: 6px; @@ -361,6 +361,11 @@ cursor: inherit; opacity: 1; outline: inherit; + .admin__action-multiselect-wrap { + .admin__action-multiselect { + .__form-control-pattern__disabled(); + } + } } &._hidden { @@ -545,7 +550,6 @@ & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); cursor: pointer; - background: @color-white; left: 0; position: absolute; top: 0; @@ -664,7 +668,7 @@ display: inline-block; } } - + + .admin__field:last-child { width: auto; @@ -700,7 +704,7 @@ width: 100%; } } - & > .admin__field-label { + & > .admin__field-label { text-align: left; } diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 53af8933343f1..1703e87691788 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -1827,6 +1827,10 @@ background-image: url(../images/fam_application_form_delete.png); } + .x-tree-node .x-tree-node-el input[type=checkbox] { + margin-left: 3px; + } + // // Styles for "js" tooltip with positionings // -------------------------------------- @@ -3958,6 +3962,22 @@ .grid tr.headings th > span { white-space: normal; } + + .field { + &.field-subscription { + .admin__field-label { + margin-left: 10px; + float: none; + cursor: pointer; + } + + .admin__field-control { + float: left; + width: auto; + margin: 6px 0px 0px 0px; + } + } + } } } @@ -4060,6 +4080,31 @@ } } +.newsletter-template-preview { + height: 100%; + .cms-revision-preview { + height: 100%; + .preview_iframe { + height: calc(~'100% - 50px'); + } + } +} + +.adminhtml-email_template-preview { + .cms-revision-preview { + padding-top: 56.25%; + position: relative; + + #preview_iframe { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } +} + .admin__scope-old { .buttons-set { margin: 0 0 15px; diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 39b364ea8553f..05d73ac20fcbd 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -93,6 +93,7 @@ define('globalNavigationScroll', [ } else { // static menu cases checkRemoveClass(menu, fixedClassName); + menu.css('top', 'auto'); } // Save previous window scrollTop diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index 299c138832064..44e93087399a1 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -457,9 +457,11 @@ .action { &.delete { &:extend(.abs-remove-button-for-blocks all); + line-height: unset; position: absolute; right: 0; - top: 0; + top: -1px; + width: auto; } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_cart.less index d3d15019f0e87..3142d5de64be1 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_cart.less @@ -346,6 +346,10 @@ .widget { float: left; + + &.block { + margin-bottom: @indent__base; + } } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less index 65f3eeef63b01..133dd0fe721bb 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less @@ -110,7 +110,7 @@ @_dropdown-list-position-right: 0, @_dropdown-list-pointer-position: right, @_dropdown-list-pointer-position-left-right: 26px, - @_dropdown-list-z-index: 101, + @_dropdown-list-z-index: 101, @_dropdown-toggle-icon-content: @icon-cart, @_dropdown-toggle-active-icon-content: @icon-cart, @_dropdown-list-item-padding: false, @@ -263,6 +263,7 @@ ); cursor: pointer; position: relative; + white-space: nowrap; &:after { position: static; @@ -304,7 +305,7 @@ .weee[data-label] { .lib-font-size(11); - + .label { &:extend(.abs-no-display all); } @@ -340,7 +341,6 @@ } .item-qty { - margin-right: @indent__s; text-align: center; width: 45px; } @@ -390,6 +390,16 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { .minicart-wrapper { margin-top: @indent__s; + .lib-clearfix(); + .product { + .actions { + float: left; + margin: 10px 0 0 0; + } + } + .update-cart-item { + float: right; + } } } @@ -400,7 +410,7 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .minicart-wrapper { margin-left: 13px; - + .block-minicart { right: -15px; width: 390px; diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Contact/web/css/source/_module.less similarity index 100% rename from app/code/Magento/Contact/view/frontend/web/css/source/_module.less rename to app/design/frontend/Magento/blank/Magento_Contact/web/css/source/_module.less diff --git a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less index 213b8131815b3..0c2b1b4db83e6 100644 --- a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less @@ -85,6 +85,7 @@ .box-information, .box-newsletter { .box-content { + .lib-wrap-words(); line-height: 26px; } } diff --git a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less index f22a325debc9c..09759d95c4b10 100644 --- a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less @@ -44,7 +44,8 @@ } input { - padding-left: 35px; + margin-right: 35px; + padding: 0 0 0 35px; // Reset some default Safari padding values. } .title { @@ -75,7 +76,8 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .block.newsletter { - width: 32%; + max-width: 44%; + width: max-content; .field { margin-right: 5px; diff --git a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less index 678ac535d5d71..07317e1670a0b 100644 --- a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less @@ -120,7 +120,7 @@ &.selected { .lib-css(background, @attr-swatch-option__selected__background); .lib-css(border, @attr-swatch-option__selected__border); - .lib-css(color, @attr-swatch-option__selected__color); + .lib-css(color, @attr-swatch-option__selected__color); } } } @@ -180,7 +180,9 @@ } &.disabled { + box-shadow: unset; cursor: default; + pointer-events: none; &:after { // ToDo: improve .lib-background-gradient() to support diagonal gradient diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less index b314bcf5b3473..3faa8ca965410 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less @@ -89,6 +89,7 @@ img { display: block; + height: auto; } .page-print & { diff --git a/app/design/frontend/Magento/blank/etc/view.xml b/app/design/frontend/Magento/blank/etc/view.xml index e742ce0a21cd1..5884699af15cd 100644 --- a/app/design/frontend/Magento/blank/etc/view.xml +++ b/app/design/frontend/Magento/blank/etc/view.xml @@ -262,12 +262,10 @@ <item type="file">Lib::jquery/jquery.min.js</item> <item type="file">Lib::jquery/jquery-ui-1.9.2.js</item> <item type="file">Lib::jquery/jquery.details.js</item> - <item type="file">Lib::jquery/jquery.details.min.js</item> <item type="file">Lib::jquery/jquery.hoverIntent.js</item> <item type="file">Lib::jquery/colorpicker/js/colorpicker.js</item> <item type="file">Lib::requirejs/require.js</item> <item type="file">Lib::requirejs/text.js</item> - <item type="file">Lib::date-format-normalizer.js</item> <item type="file">Lib::legacy-build.min.js</item> <item type="file">Lib::mage/captcha.js</item> <item type="file">Lib::mage/dropdown_old.js</item> @@ -292,6 +290,7 @@ <item type="directory">Magento_Ui::templates/grid</item> <item type="directory">Magento_Ui::templates/dynamic-rows</item> <item type="directory">Magento_Swagger::swagger-ui</item> + <item type="directory">Magento_Tinymce3::tiny_mce</item> <item type="directory">Lib::modernizr</item> <item type="directory">Lib::tiny_mce</item> <item type="directory">Lib::varien</item> diff --git a/app/design/frontend/Magento/blank/media/preview.jpg b/app/design/frontend/Magento/blank/media/preview.jpg index 61be74876a1e9..438a72e108898 100644 Binary files a/app/design/frontend/Magento/blank/media/preview.jpg and b/app/design/frontend/Magento/blank/media/preview.jpg differ diff --git a/app/design/frontend/Magento/blank/web/css/source/_extends.less b/app/design/frontend/Magento/blank/web/css/source/_extends.less index a969d3499b51c..5bdaa4c3c35a3 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_extends.less +++ b/app/design/frontend/Magento/blank/web/css/source/_extends.less @@ -1290,7 +1290,7 @@ } // -// Shopping cart sidebar and checkout sidebar totals +// Mini Cart and checkout sidebar totals // --------------------------------------------- & when (@media-common = true) { diff --git a/app/design/frontend/Magento/blank/web/css/source/_layout.less b/app/design/frontend/Magento/blank/web/css/source/_layout.less index 62195b5566f8e..287f1b45b8328 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_layout.less +++ b/app/design/frontend/Magento/blank/web/css/source/_layout.less @@ -125,11 +125,14 @@ } .page-layout-2columns-left { + .main { + padding-left: @layout-column__additional-sidebar-offset + } + .sidebar-additional { clear: left; float: left; padding-left: 0; - padding-right: @layout-column__additional-sidebar-offset; } } diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index 3b4da1d1ae6f5..9b6986249b009 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -397,7 +397,7 @@ .box-tocart { &:extend(.abs-box-tocart all); - + .field.qty { } @@ -987,6 +987,24 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__s) { + .sidebar { + .product-items { + .action { + &.delete { + &:extend(.abs-remove-button-for-blocks all); + line-height: unset; + position: absolute; + right: 0; + top: -1px; + width: auto; + } + } + } + } +} + .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .compare.wrapper, [class*='block-compare'] { diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less index 92945d61e4368..7745900f1766c 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less @@ -292,7 +292,7 @@ margin: -10px; padding: 9px; position: relative; - z-index: 2; + z-index: 9; .product-item-inner { display: block; @@ -360,13 +360,12 @@ position: absolute; top: -2px; width: 100%; - z-index: 1; + z-index: -1; } } } .product-item-actions { - position: relative; z-index: 1; } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 460a961830b43..cbf1d185a5a08 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -563,6 +563,10 @@ .widget { float: left; + + &.block { + margin-bottom: @indent__base; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less index af94dd7b97bbb..43ccee738a45e 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less @@ -120,7 +120,7 @@ @_dropdown-list-position-right: -10px, @_dropdown-list-pointer-position: right, @_dropdown-list-pointer-position-left-right: 12px, - @_dropdown-list-z-index: 101, + @_dropdown-list-z-index: 101, @_dropdown-toggle-icon-content: @icon-cart, @_dropdown-toggle-active-icon-content: @icon-cart, @_dropdown-list-item-padding: false, @@ -136,7 +136,7 @@ .block-minicart { .lib-css(padding, 25px @minicart__padding-horizontal); - + .block-title { display: none; } @@ -233,7 +233,7 @@ .minicart-items { .lib-list-reset-styles(); - + .product-item { padding: @indent__base 0; @@ -315,8 +315,9 @@ .toggle { &:extend(.abs-toggling-title all); border: 0; - padding: 0 @indent__xl @indent__xs 0; - + padding: 0 0 @indent__xs 0; + white-space: nowrap; + &:after { .lib-css(color, @color-gray56); margin: 0 0 0 @indent__xs; @@ -349,7 +350,7 @@ @_icon-font-position: after ); } - + > span { &:extend(.abs-visually-hidden-reset all); } @@ -369,7 +370,6 @@ } .item-qty { - margin-right: @indent__s; text-align: center; width: 60px; } @@ -419,6 +419,16 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .minicart-wrapper { margin-top: @indent__s; + .lib-clearfix(); + .product { + .actions { + float: left; + margin: 10px 0 0 0; + } + } + .update-cart-item { + float: right; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less index a2daf0da247d1..ce3f45c990b96 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less @@ -78,27 +78,28 @@ } .abs-discount-code { - .actions-toolbar { - display: table-cell; - vertical-align: top; - width: 1%; - - .primary { - float: left; - .action { - &:extend(.abs-revert-to-action-secondary all); - border-bottom-left-radius: 0; - border-top-left-radius: 0; - margin: 0 0 0 -2px; - white-space: nowrap; - width: auto; - } - } - } - .form-discount { + .form-discount { display: table; width: 100%; - + + .actions-toolbar { + display: table-cell; + vertical-align: top; + width: 1%; + + .primary { + float: left; + .action { + &:extend(.abs-revert-to-action-secondary all); + border-bottom-left-radius: 0; + border-top-left-radius: 0; + margin: 0 0 0 -2px; + white-space: nowrap; + width: auto; + } + } + } + > .field { > .label { display: none; diff --git a/app/design/frontend/Magento/luma/Magento_Customer/email/account_new.html b/app/design/frontend/Magento/luma/Magento_Customer/email/account_new.html index c8b10aa6dffc7..4a897a62a9235 100644 --- a/app/design/frontend/Magento/luma/Magento_Customer/email/account_new.html +++ b/app/design/frontend/Magento/luma/Magento_Customer/email/account_new.html @@ -4,17 +4,19 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", "var customer.email":"Customer Email", -"var customer.name":"Customer Name" +"var customer.name":"Customer Name", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Welcome to %store_name." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site, use these credentials during checkout or on the <a href="%customer_url">My Account</a> page:' diff --git a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index 6adf4b5b2f86b..6354cc35d32ed 100644 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -126,6 +126,7 @@ .box-information, .box-newsletter { .box-content { + .lib-wrap-words(); &:extend(.abs-account-block-line-height all); } } diff --git a/app/design/frontend/Magento/luma/Magento_Email/email/footer.html b/app/design/frontend/Magento/luma/Magento_Email/email/footer.html index 0fc8e36a82076..9620e5c446e60 100644 --- a/app/design/frontend/Magento/luma/Magento_Email/email/footer.html +++ b/app/design/frontend/Magento/luma/Magento_Email/email/footer.html @@ -6,7 +6,12 @@ --> <!--@subject {{trans "Footer"}} @--> <!--@vars { -"var store.getFrontendName()":"Store Name" +"var store.frontend_name":"Store Name", +"var url_about_us":"About Us URL", +"var url_customer_service":"Customer Service URL", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours", +"var store.formatted_address|raw":"Store Address" } @--> <!-- End Content --> @@ -42,7 +47,7 @@ </td> <td> <p class="address"> - {{var store.getFormattedAddress()|raw}} + {{var store.formatted_address|raw}} </p> </td> </tr> diff --git a/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less index ff377a4b88acc..0f2a0db56a355 100644 --- a/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less @@ -328,7 +328,7 @@ .gift-options-cart-item { & + .towishlist { - left: 43px; + left: 0; position: absolute; } } diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index d7ee1319c9a43..5d44a32b9391b 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -46,7 +46,8 @@ } input { - padding-left: 35px; + margin-right: 35px; + padding: 0 0 0 35px; // Reset some default Safari padding values. } .title { @@ -78,7 +79,8 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .block.newsletter { - width: 34%; + max-width: 44%; + width: max-content; } } diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new.html index 098a610adaddf..86e3cf01e965e 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new.html @@ -4,28 +4,32 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var creditmemo":"Credit Memo", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> <p> @@ -55,7 +59,7 @@ <h1>{{trans "Your Credit Memo #%creditmemo_id for Order #%order_id" creditmemo_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -67,10 +71,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html index f116e86eeeb06..d0310a8e2c7b6 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.getName()":"Guest Customer Name (Billing)", +"var billing.name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var creditmemo":"Credit Memo", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> <p> {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>.' store_email=$store_email |raw}} @@ -53,7 +57,7 @@ <h1>{{trans "Your Credit Memo #%creditmemo_id for Order #%order_id" creditmemo_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -65,10 +69,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html index 269e46d752084..352c9ab4fddf4 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html @@ -4,27 +4,29 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.frontend_name}} @--> <!--@vars { -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html index c8bdae7b08fa5..9b09760c1fa4a 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html @@ -4,26 +4,28 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.frontend_name}} @--> <!--@vars { -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.getName()":"Guest Customer Name", +"var billing.name":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store_email":"Store Email", +"var store.frontend_name":"Store Frontend Name" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new.html index 8d7ba99312375..636fa9ac5f425 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new.html @@ -4,28 +4,32 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Invoice Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout area=\"frontend\" handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var invoice": "Invoice", +"var order": "Order", +"var order_data.is_not_virtual": "Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> <p> @@ -55,7 +59,7 @@ <h1>{{trans "Your Invoice #%invoice_id for Order #%order_id" invoice_id=$invoice <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -67,10 +71,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html index b8c604daf824b..7df5ffe5f4ab8 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.getName()":"Guest Customer Name", -"var comment":"Invoice Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var invoice": "Invoice", +"var order": "Order", +"var order_data.is_not_virtual": "Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> <p> {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>.' store_email=$store_email |raw}} @@ -53,7 +57,7 @@ <h1>{{trans "Your Invoice #%invoice_id for Order #%order_id" invoice_id=$invoice <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -65,10 +69,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html index 8ec54f1e64d9c..834b512f82fb7 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html @@ -4,27 +4,29 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Invoice Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html index 6028db7b97730..f9e1498763cba 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html @@ -4,26 +4,28 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Invoice Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new.html index b663dc92d1af8..745bf5c9c2eff 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new.html @@ -4,16 +4,23 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var order.getEmailCustomerNote()":"Email Order Note", +"var order_data.email_customer_note|escape|nl2br":"Email Order Note", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order area=\"frontend\"":"Order Items Grid", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var shipping_msg":"Shipping message" +"var order.shipping_description":"Shipping Description", +"var shipping_msg":"Shipping message", +"var created_at_formatted":"Order Created At (datetime)", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type", +"var order_data.customer_name":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL" } @--> {{template config_path="design/email/header_template"}} @@ -21,9 +28,9 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%customer_name," customer_name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%customer_name," customer_name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> @@ -35,16 +42,16 @@ <tr class="email-summary"> <td> <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_id=$order.increment_id |raw}}</h1> - <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$order.getCreatedAtFormatted(1) |raw}}</p> + <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$created_at_formatted |raw}}</p> </td> </tr> <tr class="email-information"> <td> - {{depend order.getEmailCustomerNote()}} + {{depend order_data.email_customer_note}} <table class="message-info"> <tr> <td> - {{var order.getEmailCustomerNote()|escape|nl2br}} + {{var order_data.email_customer_note|escape|nl2br}} </td> </tr> </table> @@ -55,7 +62,7 @@ <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -67,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> {{if shipping_msg}} <p>{{var shipping_msg}}</p> {{/if}} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html index cc32aa4a12676..907be4d45a6c5 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var order.getEmailCustomerNote()":"Email Order Note", -"var order.getBillingAddress().getName()":"Guest Customer Name", -"var order.getCreatedAtFormatted(1)":"Order Created At (datetime)", +"var order_data.email_customer_note|escape|nl2br":"Email Order Note", +"var order.billing_address.name":"Guest Customer Name", +"var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var shipping_msg":"Shipping message" +"var order.shipping_description":"Shipping Description", +"var shipping_msg":"Shipping message", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getBillingAddress().getName()}}</p> + <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} </p> <p> @@ -35,16 +39,16 @@ <tr class="email-summary"> <td> <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_id=$order.increment_id |raw}}</h1> - <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$order.getCreatedAtFormatted(1) |raw}}</p> + <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$created_at_formatted |raw}}</p> </td> </tr> <tr class="email-information"> <td> - {{depend order.getEmailCustomerNote()}} + {{depend order_data.email_customer_note}} <table class="message-info"> <tr> <td> - {{var order.getEmailCustomerNote()|escape|nl2br}} + {{var order_data.email_customer_note|escape|nl2br}} </td> </tr> </table> @@ -55,7 +59,7 @@ <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -67,10 +71,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> {{if shipping_msg}} <p>{{var shipping_msg}}</p> {{/if}} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html index fa16ac2196bf4..cec5649755b3d 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Order Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html index 8ead615fe01ca..5f23898b50018 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html @@ -4,25 +4,27 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Order Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html index e467aa843e2f4..4ff9da3a31b27 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html @@ -4,29 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", -"var comment":"Shipment Comment", +"var comment|escape|nl2br":"Shipment Comment", "var shipment.increment_id":"Shipment Id", "layout handle=\"sales_email_order_shipment_items\" shipment=$shipment order=$order":"Shipment Items Grid", "block class='Magento\\\\Framework\\\\View\\\\Element\\\\Template' area='frontend' template='Magento_Sales::email\/shipment\/track.phtml' shipment=$shipment order=$order":"Shipment Track Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var order_data.is_not_virtual": "Order Type", +"var shipment": "Shipment", +"var order": "Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> <p> @@ -58,7 +62,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -70,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html index 385110f8f037e..ac7eaae6b7ff7 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html @@ -4,28 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.getName()":"Guest Customer Name", +"var billing.name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", -"var comment":"Shipment Comment", +"var comment|escape|nl2br":"Shipment Comment", "var shipment.increment_id":"Shipment Id", "layout handle=\"sales_email_order_shipment_items\" shipment=$shipment order=$order":"Shipment Items Grid", "block class='Magento\\\\Framework\\\\View\\\\Element\\\\Template' area='frontend' template='Magento_Sales::email\/shipment\/track.phtml' shipment=$shipment order=$order":"Shipment Track Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var order_data.is_not_virtual": "Order Type", +"var shipment": "Shipment", +"var order": "Order", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> <p> {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>.' store_email=$store_email |raw}} @@ -56,7 +61,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +73,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html index 4f9b7286f3ae4..f1bd61ebec3dd 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html @@ -4,27 +4,29 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Order Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status", -"var shipment.increment_id":"Shipment Id" +"var order_data.frontend_status_label":"Order Status", +"var shipment.increment_id":"Shipment Id", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html index 3ef26463ea755..5887ff73c6398 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html @@ -4,26 +4,28 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Order Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getFrontendStatusLabel()":"Order Status", -"var shipment.increment_id":"Shipment Id" +"var order_data.frontend_status_label":"Order Status", +"var shipment.increment_id":"Shipment Id", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getFrontendStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index cc6aba6f3566e..599218f970907 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less @@ -66,6 +66,10 @@ &:not(:last-child) { margin-bottom: @indent__l; } + + &.order-items-shipment { + overflow: visible; + } } .table-order-items { diff --git a/app/design/frontend/Magento/luma/Magento_Theme/layout/default.xml b/app/design/frontend/Magento/luma/Magento_Theme/layout/default.xml index 5f0f3b92ab44a..ce397fad64f44 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/layout/default.xml +++ b/app/design/frontend/Magento/luma/Magento_Theme/layout/default.xml @@ -14,12 +14,6 @@ </arguments> </block> </referenceContainer> - <referenceBlock name="logo"> - <arguments> - <argument name="logo_img_width" xsi:type="number">148</argument> - <argument name="logo_img_height" xsi:type="number">43</argument> - </arguments> - </referenceBlock> <referenceContainer name="footer"> <block class="Magento\Store\Block\Switcher" name="store_switcher" as="store_switcher" after="footer_links" template="Magento_Store::switch/stores.phtml"/> </referenceContainer> diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index b841f2206e1d9..438fb55d32e5c 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less @@ -148,6 +148,7 @@ img { display: block; + height: auto; } .page-print & { diff --git a/app/design/frontend/Magento/luma/etc/view.xml b/app/design/frontend/Magento/luma/etc/view.xml index 7aa2e51481bd9..a2802b7e374f3 100644 --- a/app/design/frontend/Magento/luma/etc/view.xml +++ b/app/design/frontend/Magento/luma/etc/view.xml @@ -273,12 +273,10 @@ <item type="file">Lib::jquery/jquery.min.js</item> <item type="file">Lib::jquery/jquery-ui-1.9.2.js</item> <item type="file">Lib::jquery/jquery.details.js</item> - <item type="file">Lib::jquery/jquery.details.min.js</item> <item type="file">Lib::jquery/jquery.hoverIntent.js</item> <item type="file">Lib::jquery/colorpicker/js/colorpicker.js</item> <item type="file">Lib::requirejs/require.js</item> <item type="file">Lib::requirejs/text.js</item> - <item type="file">Lib::date-format-normalizer.js</item> <item type="file">Lib::legacy-build.min.js</item> <item type="file">Lib::mage/captcha.js</item> <item type="file">Lib::mage/dropdown_old.js</item> @@ -303,6 +301,7 @@ <item type="directory">Magento_Ui::templates/grid</item> <item type="directory">Magento_Ui::templates/dynamic-rows</item> <item type="directory">Magento_Swagger::swagger-ui</item> + <item type="directory">Magento_Tinymce3::tiny_mce</item> <item type="directory">Lib::modernizr</item> <item type="directory">Lib::tiny_mce</item> <item type="directory">Lib::varien</item> diff --git a/app/design/frontend/Magento/luma/media/preview.jpg b/app/design/frontend/Magento/luma/media/preview.jpg index feeda8840e562..fa6356fa02c35 100644 Binary files a/app/design/frontend/Magento/luma/media/preview.jpg and b/app/design/frontend/Magento/luma/media/preview.jpg differ diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index 81b7716d61ab7..ce86b690f6252 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -1713,7 +1713,7 @@ } // -// Shopping cart sidebar and checkout sidebar totals +// Mini Cart and checkout sidebar totals // --------------------------------------------- & when (@media-common = true) { diff --git a/app/etc/di.xml b/app/etc/di.xml index 50088d41f1b4b..dfbf4f2373ac5 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -47,6 +47,7 @@ <preference for="Magento\Framework\App\RequestSafetyInterface" type="Magento\Framework\App\Request\Http" /> <preference for="\Magento\Framework\Setup\SchemaSetupInterface" type="\Magento\Setup\Module\Setup" /> <preference for="\Magento\Framework\Setup\ModuleDataSetupInterface" type="\Magento\Setup\Module\DataSetup" /> + <preference for="Magento\Framework\App\ExceptionHandlerInterface" type="Magento\Framework\App\ExceptionHandler" /> <type name="Magento\Store\Model\Store"> <arguments> <argument name="currencyInstalled" xsi:type="string">system/currency/installed</argument> @@ -65,7 +66,6 @@ <preference for="Magento\Framework\Config\CacheInterface" type="Magento\Framework\App\Cache\Type\Config" /> <preference for="Magento\Framework\Config\ValidationStateInterface" type="Magento\Framework\App\Arguments\ValidationState" /> <preference for="Magento\Framework\Module\ModuleListInterface" type="Magento\Framework\Module\ModuleList" /> - <preference for="Magento\Framework\Module\ModuleManagerInterface" type="Magento\Framework\Module\Manager" /> <preference for="Magento\Framework\Component\ComponentRegistrarInterface" type="Magento\Framework\Component\ComponentRegistrar"/> <preference for="Magento\Framework\Event\ConfigInterface" type="Magento\Framework\Event\Config" /> <preference for="Magento\Framework\Event\InvokerInterface" type="Magento\Framework\Event\Invoker\InvokerDefault" /> @@ -663,6 +663,14 @@ <argument name="argumentInterpreter" xsi:type="object">layoutArgumentGeneratorInterpreter</argument> </arguments> </type> + <type name="Magento\Framework\View\Element\UiComponent\Argument\Interpreter\ConfigurableObject"> + <arguments> + <argument name="classWhitelist" xsi:type="array"> + <item name="0" xsi:type="string">Magento\Framework\Data\OptionSourceInterface</item> + <item name="1" xsi:type="string">Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\Mview\View"> <arguments> <argument name="state" xsi:type="object" shared="false">Magento\Indexer\Model\Mview\View\State</argument> @@ -1494,6 +1502,16 @@ </argument> </arguments> </type> + <virtualType name="Magento\Framework\Config\ValidationState\Required" type="Magento\Framework\Config\ValidationState\Configurable"> + <arguments> + <argument name="required" xsi:type="boolean">true</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Framework\Config\ValidationState\NotRequired" type="Magento\Framework\Config\ValidationState\Configurable"> + <arguments> + <argument name="required" xsi:type="boolean">false</argument> + </arguments> + </virtualType> <virtualType name="Magento\Framework\Setup\Declaration\Schema\Config\SchemaLocator" type="Magento\Framework\Config\SchemaLocator"> <arguments> <argument name="realPath" xsi:type="string">urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd</argument> @@ -1780,4 +1798,6 @@ <type name="Magento\Framework\DB\Adapter\AdapterInterface"> <plugin name="execute_commit_callbacks" type="Magento\Framework\Model\ExecuteCommitCallbacks" /> </type> + <preference for="Magento\Framework\GraphQl\Query\ErrorHandlerInterface" type="Magento\Framework\GraphQl\Query\ErrorHandler"/> + <preference for="Magento\Framework\Filter\VariableResolverInterface" type="Magento\Framework\Filter\VariableResolver\StrategyResolver"/> </config> diff --git a/app/functions.php b/app/functions.php index 4b00d01819f70..6b3dae71c42c6 100644 --- a/app/functions.php +++ b/app/functions.php @@ -13,6 +13,9 @@ * @return \Magento\Framework\Phrase */ if (!function_exists('__')) { + /** + * @return \Magento\Framework\Phrase + */ function __() { $argc = func_get_args(); diff --git a/composer.json b/composer.json index c78ae2fdffda1..4e78f54942576 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,6 @@ "ext-pdo_mysql": "*", "ext-simplexml": "*", "ext-soap": "*", - "ext-spl": "*", "ext-xsl": "*", "ext-zip": "*", "lib-libxml": "*", @@ -35,7 +34,7 @@ "colinmollenhour/credis": "1.10.0", "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", - "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1", + "elasticsearch/elasticsearch": "~2.0||~5.1||~6.1", "magento/composer": "~1.5.0", "magento/magento-composer-installer": ">=0.1.11", "magento/zendframework1": "~1.14.2", @@ -43,16 +42,16 @@ "wikimedia/less.php": "~1.8.0", "paragonie/sodium_compat": "^1.6", "pelago/emogrifier": "^2.0.0", - "php-amqplib/php-amqplib": "~2.7.0", + "php-amqplib/php-amqplib": "~2.7.0||~2.10.0", "phpseclib/mcrypt_compat": "1.0.8", "phpseclib/phpseclib": "2.0.*", "ramsey/uuid": "~3.8.0", - "symfony/console": "~4.1.0|~4.2.0|~4.3.0", - "symfony/event-dispatcher": "~4.1.0|~4.2.0|~4.3.0", - "symfony/process": "~4.1.0|~4.2.0|~4.3.0", + "symfony/console": "~4.1.0||~4.2.0||~4.3.0", + "symfony/event-dispatcher": "~4.1.0||~4.2.0||~4.3.0", + "symfony/process": "~4.1.0||~4.2.0||~4.3.0", "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", - "webonyx/graphql-php": "^0.12.6", + "webonyx/graphql-php": "^0.13.8", "zendframework/zend-captcha": "^2.7.1", "zendframework/zend-code": "~3.3.0", "zendframework/zend-config": "^2.6.0", @@ -85,11 +84,13 @@ }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", "friendsofphp/php-cs-fixer": "~2.14.0", "lusitanian/oauth": "~0.8.10", - "magento/magento-coding-standard": "~4.0.0", - "magento/magento2-functional-testing-framework": "2.4.3", + "magento/magento-coding-standard": "*", + "magento/magento2-functional-testing-framework": "2.5.3", "pdepend/pdepend": "2.5.2", + "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "@stable", "phpunit/phpunit": "~6.5.0", "sebastian/phpcpd": "~3.0.0", @@ -100,6 +101,7 @@ }, "replace": { "magento/module-marketplace": "*", + "magento/module-admin-analytics": "*", "magento/module-admin-notification": "*", "magento/module-advanced-pricing-import-export": "*", "magento/module-amqp": "*", @@ -123,6 +125,7 @@ "magento/module-captcha": "*", "magento/module-cardinal-commerce": "*", "magento/module-catalog": "*", + "magento/module-catalog-customer-graph-ql": "*", "magento/module-catalog-analytics": "*", "magento/module-catalog-import-export": "*", "magento/module-catalog-inventory": "*", @@ -170,6 +173,7 @@ "magento/module-graph-ql": "*", "magento/module-graph-ql-cache": "*", "magento/module-catalog-graph-ql": "*", + "magento/module-catalog-cms-graph-ql": "*", "magento/module-catalog-url-rewrite-graph-ql": "*", "magento/module-configurable-product-graph-ql": "*", "magento/module-customer-graph-ql": "*", @@ -189,6 +193,8 @@ "magento/module-instant-purchase": "*", "magento/module-integration": "*", "magento/module-layered-navigation": "*", + "magento/module-media-gallery": "*", + "magento/module-media-gallery-api": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", "magento/module-msrp": "*", @@ -283,7 +289,8 @@ "components/jqueryui": "1.10.4", "twbs/bootstrap": "3.1.0", "tinymce/tinymce": "3.4.7", - "magento/module-tinymce-3": "*" + "magento/module-tinymce-3": "*", + "magento/module-csp": "*" }, "conflict": { "gene/bluefoot": "*" diff --git a/composer.lock b/composer.lock index 50f0843d9e49a..ba126b3eabefc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d03365abc46d68e3654342a174871fb2", + "content-hash": "e3ad90186a7742707e4c12cda2580b35", "packages": [ { "name": "braintree/braintree_php", @@ -201,16 +201,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.1", + "version": "1.2.4", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "33810d865dd06a674130fceb729b2f279dc79e8c" + "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/33810d865dd06a674130fceb729b2f279dc79e8c", - "reference": "33810d865dd06a674130fceb729b2f279dc79e8c", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527", + "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527", "shasum": "" }, "require": { @@ -253,20 +253,20 @@ "ssl", "tls" ], - "time": "2019-07-31T08:13:16+00:00" + "time": "2019-08-30T08:44:50+00:00" }, { "name": "composer/composer", - "version": "1.8.6", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "19b5f66a0e233eb944f134df34091fe1c5dfcc11" + "reference": "bb01f2180df87ce7992b8331a68904f80439dd2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/19b5f66a0e233eb944f134df34091fe1c5dfcc11", - "reference": "19b5f66a0e233eb944f134df34091fe1c5dfcc11", + "url": "https://api.github.com/repos/composer/composer/zipball/bb01f2180df87ce7992b8331a68904f80439dd2f", + "reference": "bb01f2180df87ce7992b8331a68904f80439dd2f", "shasum": "" }, "require": { @@ -302,7 +302,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -326,14 +326,14 @@ "homepage": "http://seld.be" } ], - "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", "homepage": "https://getcomposer.org/", "keywords": [ "autoload", "dependency", "package" ], - "time": "2019-06-11T13:03:06+00:00" + "time": "2019-11-01T16:20:17+00:00" }, { "name": "composer/semver", @@ -459,24 +459,24 @@ }, { "name": "composer/xdebug-handler", - "version": "1.3.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f" + "reference": "cbe23383749496fe0f373345208b79568e4bc248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f", - "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/cbe23383749496fe0f373345208b79568e4bc248", + "reference": "cbe23383749496fe0f373345208b79568e4bc248", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0", + "php": "^5.3.2 || ^7.0 || ^8.0", "psr/log": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" }, "type": "library", "autoload": { @@ -494,12 +494,12 @@ "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Restarts a process without xdebug.", + "description": "Restarts a process without Xdebug.", "keywords": [ "Xdebug", "performance" ], - "time": "2019-05-27T17:52:04+00:00" + "time": "2019-11-06T16:40:04+00:00" }, { "name": "container-interop/container-interop", @@ -530,6 +530,7 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", + "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, { @@ -594,27 +595,28 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.3.3", + "version": "6.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + "reference": "0895c932405407fd3a7368b6910c09a24d26db11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0895c932405407fd3a7368b6910c09a24d26db11", + "reference": "0895c932405407fd3a7368b6910c09a24d26db11", "shasum": "" }, "require": { + "ext-json": "*", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", + "guzzlehttp/psr7": "^1.6.1", "php": ">=5.5" }, "require-dev": { "ext-curl": "*", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.0" + "psr/log": "^1.1" }, "suggest": { "psr/log": "Required for using the Log middleware" @@ -626,12 +628,12 @@ } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\": "src/" - } + }, + "files": [ + "src/functions_include.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -655,7 +657,7 @@ "rest", "web service" ], - "time": "2018-04-22T15:46:56+00:00" + "time": "2019-10-23T15:58:00+00:00" }, { "name": "guzzlehttp/promises", @@ -882,23 +884,23 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.8", + "version": "5.2.9", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4" + "reference": "44c6787311242a979fa15c704327c20e7221a0e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4", - "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4", + "reference": "44c6787311242a979fa15c704327c20e7221a0e4", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20", + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", "json-schema/json-schema-test-suite": "1.2.0", "phpunit/phpunit": "^4.8.35" }, @@ -944,7 +946,7 @@ "json", "schema" ], - "time": "2019-01-14T23:55:14+00:00" + "time": "2019-09-25T14:49:45+00:00" }, { "name": "magento/composer", @@ -1110,16 +1112,16 @@ }, { "name": "monolog/monolog", - "version": "1.24.0", + "version": "1.25.2", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" + "reference": "d5e2fb341cb44f7e2ab639d12a1e5901091ec287" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d5e2fb341cb44f7e2ab639d12a1e5901091ec287", + "reference": "d5e2fb341cb44f7e2ab639d12a1e5901091ec287", "shasum": "" }, "require": { @@ -1184,7 +1186,7 @@ "logging", "psr-3" ], - "time": "2018-11-05T09:00:11+00:00" + "time": "2019-11-13T10:00:05+00:00" }, { "name": "paragonie/random_compat", @@ -1233,16 +1235,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.10.1", + "version": "v1.12.1", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "5115fa44886d1c2785d2f135ef4626db868eac4b" + "reference": "063cae9b3a7323579063e7037720f5b52b56c178" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/5115fa44886d1c2785d2f135ef4626db868eac4b", - "reference": "5115fa44886d1c2785d2f135ef4626db868eac4b", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/063cae9b3a7323579063e7037720f5b52b56c178", + "reference": "063cae9b3a7323579063e7037720f5b52b56c178", "shasum": "" }, "require": { @@ -1250,7 +1252,7 @@ "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" }, "require-dev": { - "phpunit/phpunit": "^3|^4|^5" + "phpunit/phpunit": "^3|^4|^5|^6|^7" }, "suggest": { "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", @@ -1311,20 +1313,20 @@ "secret-key cryptography", "side-channel resistant" ], - "time": "2019-07-12T16:36:59+00:00" + "time": "2019-11-07T17:07:24+00:00" }, { "name": "pelago/emogrifier", - "version": "v2.1.1", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/emogrifier.git", - "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983" + "reference": "2472bc1c3a2dee8915ecc2256139c6100024332f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8ee7fb5ad772915451ed3415c1992bd3697d4983", - "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/2472bc1c3a2dee8915ecc2256139c6100024332f", + "reference": "2472bc1c3a2dee8915ecc2256139c6100024332f", "shasum": "" }, "require": { @@ -1342,7 +1344,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1355,16 +1357,6 @@ "MIT" ], "authors": [ - { - "name": "John Reeve", - "email": "jreeve@pelagodesign.com" - }, - { - "name": "Cameron Brooks" - }, - { - "name": "Jaime Prado" - }, { "name": "Oliver Klee", "email": "github@oliverklee.de" @@ -1373,9 +1365,19 @@ "name": "Zoli Szabó", "email": "zoli.szabo+github@gmail.com" }, + { + "name": "John Reeve", + "email": "jreeve@pelagodesign.com" + }, { "name": "Jake Hotson", "email": "jake@qzdesign.co.uk" + }, + { + "name": "Cameron Brooks" + }, + { + "name": "Jaime Prado" } ], "description": "Converts CSS styles into inline style attributes in your HTML code", @@ -1385,43 +1387,40 @@ "email", "pre-processing" ], - "time": "2018-12-10T10:36:30+00:00" + "time": "2019-09-04T16:07:59+00:00" }, { "name": "php-amqplib/php-amqplib", - "version": "v2.7.3", + "version": "v2.10.1", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "a8ba54bd35b973fc6861e4c2e105f71e9e95f43f" + "reference": "6e2b2501e021e994fb64429e5a78118f83b5c200" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/a8ba54bd35b973fc6861e4c2e105f71e9e95f43f", - "reference": "a8ba54bd35b973fc6861e4c2e105f71e9e95f43f", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/6e2b2501e021e994fb64429e5a78118f83b5c200", + "reference": "6e2b2501e021e994fb64429e5a78118f83b5c200", "shasum": "" }, "require": { "ext-bcmath": "*", - "ext-mbstring": "*", - "php": ">=5.3.0" + "ext-sockets": "*", + "php": ">=5.6" }, "replace": { "videlalvaro/php-amqplib": "self.version" }, "require-dev": { - "phpdocumentor/phpdocumentor": "^2.9", - "phpunit/phpunit": "^4.8", - "scrutinizer/ocular": "^1.1", + "ext-curl": "*", + "nategood/httpful": "^0.2.20", + "phpunit/phpunit": "^5.7|^6.5|^7.0", "squizlabs/php_codesniffer": "^2.5" }, - "suggest": { - "ext-sockets": "Use AMQPSocketConnection" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.10-dev" } }, "autoload": { @@ -1447,6 +1446,11 @@ "name": "Raúl Araya", "email": "nubeiro@gmail.com", "role": "Maintainer" + }, + { + "name": "Luke Bakken", + "email": "luke@bakken.io", + "role": "Maintainer" } ], "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", @@ -1456,7 +1460,7 @@ "queue", "rabbitmq" ], - "time": "2018-04-30T03:54:54+00:00" + "time": "2019-10-10T13:23:40+00:00" }, { "name": "phpseclib/mcrypt_compat", @@ -1509,16 +1513,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.21", + "version": "2.0.23", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "9f1287e68b3f283339a9f98f67515dd619e5bf9d" + "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9f1287e68b3f283339a9f98f67515dd619e5bf9d", - "reference": "9f1287e68b3f283339a9f98f67515dd619e5bf9d", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/c78eb5058d5bb1a183133c36d4ba5b6675dfa099", + "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099", "shasum": "" }, "require": { @@ -1597,7 +1601,7 @@ "x.509", "x509" ], - "time": "2019-07-12T12:53:49+00:00" + "time": "2019-09-17T03:41:22+00:00" }, { "name": "psr/container", @@ -1700,16 +1704,16 @@ }, { "name": "psr/log", - "version": "1.1.0", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", "shasum": "" }, "require": { @@ -1718,7 +1722,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -1743,7 +1747,7 @@ "psr", "psr-3" ], - "time": "2018-11-20T15:27:04+00:00" + "time": "2019-11-01T11:05:21+00:00" }, { "name": "ralouphie/getallheaders", @@ -1915,16 +1919,16 @@ }, { "name": "seld/jsonlint", - "version": "1.7.1", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38" + "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38", - "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/e2e5d290e4d2a4f0eb449f510071392e00e10d19", + "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19", "shasum": "" }, "require": { @@ -1960,7 +1964,7 @@ "parser", "validator" ], - "time": "2018-01-24T12:46:19+00:00" + "time": "2019-10-24T14:27:39+00:00" }, { "name": "seld/phar-utils", @@ -2079,16 +2083,16 @@ }, { "name": "symfony/css-selector", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d" + "reference": "f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/105c98bb0c5d8635bea056135304bd8edcc42b4d", - "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9", + "reference": "f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9", "shasum": "" }, "require": { @@ -2128,20 +2132,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2019-01-16T21:53:39+00:00" + "time": "2019-10-02T08:36:26+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "212b020949331b6531250584531363844b34a94e" + "reference": "0df002fd4f500392eabd243c2947061a50937287" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/212b020949331b6531250584531363844b34a94e", - "reference": "212b020949331b6531250584531363844b34a94e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0df002fd4f500392eabd243c2947061a50937287", + "reference": "0df002fd4f500392eabd243c2947061a50937287", "shasum": "" }, "require": { @@ -2198,20 +2202,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2019-06-27T06:42:14+00:00" + "time": "2019-11-03T09:04:05+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.5", + "version": "v1.1.7", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c61766f4440ca687de1084a5c00b08e167a2575c" + "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c", - "reference": "c61766f4440ca687de1084a5c00b08e167a2575c", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", "shasum": "" }, "require": { @@ -2256,20 +2260,20 @@ "interoperability", "standards" ], - "time": "2019-06-20T06:46:26+00:00" + "time": "2019-09-17T09:54:03+00:00" }, { "name": "symfony/filesystem", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d" + "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d", - "reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263", + "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263", "shasum": "" }, "require": { @@ -2306,20 +2310,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2019-06-23T08:51:25+00:00" + "time": "2019-08-20T14:07:54+00:00" }, { "name": "symfony/finder", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2" + "reference": "72a068f77e317ae77c0a0495236ad292cfb5ce6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9638d41e3729459860bb96f6247ccb61faaa45f2", - "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2", + "url": "https://api.github.com/repos/symfony/finder/zipball/72a068f77e317ae77c0a0495236ad292cfb5ce6f", + "reference": "72a068f77e317ae77c0a0495236ad292cfb5ce6f", "shasum": "" }, "require": { @@ -2355,20 +2359,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-06-28T13:16:30+00:00" + "time": "2019-10-30T12:53:54+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "82ebae02209c21113908c229e9883c419720738a" + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", - "reference": "82ebae02209c21113908c229e9883c419720738a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", "shasum": "" }, "require": { @@ -2380,7 +2384,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2396,13 +2400,13 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, { "name": "Gert de Pagter", "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony polyfill for ctype functions", @@ -2413,20 +2417,20 @@ "polyfill", "portable" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", "shasum": "" }, "require": { @@ -2438,7 +2442,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2472,20 +2476,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/process", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c" + "reference": "3b2e0cb029afbb0395034509291f21191d1a4db0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/856d35814cf287480465bb7a6c413bb7f5f5e69c", - "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c", + "url": "https://api.github.com/repos/symfony/process/zipball/3b2e0cb029afbb0395034509291f21191d1a4db0", + "reference": "3b2e0cb029afbb0395034509291f21191d1a4db0", "shasum": "" }, "require": { @@ -2521,7 +2525,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2019-05-30T16:10:05+00:00" + "time": "2019-10-28T17:07:32+00:00" }, { "name": "tedivm/jshrink", @@ -2670,24 +2674,31 @@ }, { "name": "webonyx/graphql-php", - "version": "v0.12.6", + "version": "v0.13.8", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95" + "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", - "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/6829ae58f4c59121df1f86915fb9917a2ec595e8", + "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8", "shasum": "" }, "require": { + "ext-json": "*", "ext-mbstring": "*", - "php": ">=5.6" + "php": "^7.1||^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8", + "doctrine/coding-standard": "^6.0", + "phpbench/phpbench": "^0.14.0", + "phpstan/phpstan": "^0.11.4", + "phpstan/phpstan-phpunit": "^0.11.0", + "phpstan/phpstan-strict-rules": "^0.11.0", + "phpunit/phpcov": "^5.0", + "phpunit/phpunit": "^7.2", "psr/http-message": "^1.0", "react/promise": "2.*" }, @@ -2711,27 +2722,27 @@ "api", "graphql" ], - "time": "2018-09-02T14:59:54+00:00" + "time": "2019-08-25T10:32:47+00:00" }, { "name": "wikimedia/less.php", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/wikimedia/less.php.git", - "reference": "f0f7768f6fa8a9d2ac6a0274f6f477c72159bf9b" + "reference": "e238ad228d74b6ffd38209c799b34e9826909266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wikimedia/less.php/zipball/f0f7768f6fa8a9d2ac6a0274f6f477c72159bf9b", - "reference": "f0f7768f6fa8a9d2ac6a0274f6f477c72159bf9b", + "url": "https://api.github.com/repos/wikimedia/less.php/zipball/e238ad228d74b6ffd38209c799b34e9826909266", + "reference": "e238ad228d74b6ffd38209c799b34e9826909266", "shasum": "" }, "require": { - "php": ">=5.3" + "php": ">=7.2.9" }, "require-dev": { - "phpunit/phpunit": "~4.8.24" + "phpunit/phpunit": "7.5.14" }, "bin": [ "bin/lessc" @@ -2750,6 +2761,10 @@ "Apache-2.0" ], "authors": [ + { + "name": "Josh Schmidt", + "homepage": "https://github.com/oyejorge" + }, { "name": "Matt Agar", "homepage": "https://github.com/agar" @@ -2757,10 +2772,6 @@ { "name": "Martin Jantošovič", "homepage": "https://github.com/Mordred" - }, - { - "name": "Josh Schmidt", - "homepage": "https://github.com/oyejorge" } ], "description": "PHP port of the Javascript version of LESS http://lesscss.org (Originally maintained by Josh Schmidt)", @@ -2772,7 +2783,7 @@ "php", "stylesheet" ], - "time": "2019-01-19T01:01:33+00:00" + "time": "2019-11-06T18:30:11+00:00" }, { "name": "zendframework/zend-captcha", @@ -2834,16 +2845,16 @@ }, { "name": "zendframework/zend-code", - "version": "3.3.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-code.git", - "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb" + "reference": "936fa7ad4d53897ea3e3eb41b5b760828246a20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-code/zipball/c21db169075c6ec4b342149f446e7b7b724f95eb", - "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/936fa7ad4d53897ea3e3eb41b5b760828246a20b", + "reference": "936fa7ad4d53897ea3e3eb41b5b760828246a20b", "shasum": "" }, "require": { @@ -2851,10 +2862,10 @@ "zendframework/zend-eventmanager": "^2.6 || ^3.0" }, "require-dev": { - "doctrine/annotations": "~1.0", + "doctrine/annotations": "^1.0", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "zendframework/zend-coding-standard": "^1.0.0", + "phpunit/phpunit": "^7.5.15", + "zendframework/zend-coding-standard": "^1.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "suggest": { @@ -2877,13 +2888,13 @@ "license": [ "BSD-3-Clause" ], - "description": "provides facilities to generate arbitrary code using an object oriented interface", - "homepage": "https://github.com/zendframework/zend-code", + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", "keywords": [ + "ZendFramework", "code", - "zf2" + "zf" ], - "time": "2018-08-13T20:36:59+00:00" + "time": "2019-08-31T14:14:34+00:00" }, { "name": "zendframework/zend-config", @@ -3151,16 +3162,16 @@ }, { "name": "zendframework/zend-diactoros", - "version": "1.8.6", + "version": "1.8.7", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "20da13beba0dde8fb648be3cc19765732790f46e" + "reference": "a85e67b86e9b8520d07e6415fcbcb8391b44a75b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e", - "reference": "20da13beba0dde8fb648be3cc19765732790f46e", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/a85e67b86e9b8520d07e6415fcbcb8391b44a75b", + "reference": "a85e67b86e9b8520d07e6415fcbcb8391b44a75b", "shasum": "" }, "require": { @@ -3180,9 +3191,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev", - "dev-develop": "1.9.x-dev", - "dev-release-2.0": "2.0.x-dev" + "dev-release-1.8": "1.8.x-dev" } }, "autoload": { @@ -3211,20 +3220,20 @@ "psr", "psr-7" ], - "time": "2018-09-05T19:29:37+00:00" + "time": "2019-08-06T17:53:53+00:00" }, { "name": "zendframework/zend-escaper", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-escaper.git", - "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074" + "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/31d8aafae982f9568287cb4dce987e6aff8fd074", - "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074", + "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/3801caa21b0ca6aca57fa1c42b08d35c395ebd5f", + "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f", "shasum": "" }, "require": { @@ -3256,7 +3265,7 @@ "escaper", "zf" ], - "time": "2018-04-25T15:48:53+00:00" + "time": "2019-09-05T20:03:20+00:00" }, { "name": "zendframework/zend-eventmanager", @@ -3377,16 +3386,16 @@ }, { "name": "zendframework/zend-filter", - "version": "2.9.1", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", - "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/d78f2cdde1c31975e18b2a0753381ed7b61118ef", + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef", "shasum": "" }, "require": { @@ -3432,26 +3441,26 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a set of commonly needed data filters", + "description": "Programmatically filter and normalize data and files", "keywords": [ "ZendFramework", "filter", "zf" ], - "time": "2018-12-17T16:00:04+00:00" + "time": "2019-08-19T07:08:04+00:00" }, { "name": "zendframework/zend-form", - "version": "2.14.1", + "version": "2.14.3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-form.git", - "reference": "ff9385b7d0d93d9bdbc2aa4af82ab616dbc7d4be" + "reference": "0b1616c59b1f3df194284e26f98c81ad0c377871" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-form/zipball/ff9385b7d0d93d9bdbc2aa4af82ab616dbc7d4be", - "reference": "ff9385b7d0d93d9bdbc2aa4af82ab616dbc7d4be", + "url": "https://api.github.com/repos/zendframework/zend-form/zipball/0b1616c59b1f3df194284e26f98c81ad0c377871", + "reference": "0b1616c59b1f3df194284e26f98c81ad0c377871", "shasum": "" }, "require": { @@ -3516,7 +3525,7 @@ "form", "zf" ], - "time": "2019-02-26T18:13:31+00:00" + "time": "2019-10-04T10:46:36+00:00" }, { "name": "zendframework/zend-http", @@ -3575,16 +3584,16 @@ }, { "name": "zendframework/zend-hydrator", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-hydrator.git", - "reference": "70b02f4d8676e64af932625751750b5ca72fff3a" + "reference": "2bfc6845019e7b6d38b0ab5e55190244dc510285" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/70b02f4d8676e64af932625751750b5ca72fff3a", - "reference": "70b02f4d8676e64af932625751750b5ca72fff3a", + "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/2bfc6845019e7b6d38b0ab5e55190244dc510285", + "reference": "2bfc6845019e7b6d38b0ab5e55190244dc510285", "shasum": "" }, "require": { @@ -3609,10 +3618,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-release-1.0": "1.0.x-dev", - "dev-release-1.1": "1.1.x-dev", - "dev-master": "2.4.x-dev", - "dev-develop": "2.5.x-dev" + "dev-release-2.4": "2.4.x-dev" }, "zf": { "component": "Zend\\Hydrator", @@ -3634,20 +3640,20 @@ "hydrator", "zf" ], - "time": "2018-11-19T19:16:10+00:00" + "time": "2019-10-04T11:17:36+00:00" }, { "name": "zendframework/zend-i18n", - "version": "2.9.0", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f" + "reference": "e17a54b3aee333ab156958f570cde630acee8b07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/6d69af5a04e1a4de7250043cb1322f077a0cdb7f", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/e17a54b3aee333ab156958f570cde630acee8b07", + "reference": "e17a54b3aee333ab156958f570cde630acee8b07", "shasum": "" }, "require": { @@ -3655,7 +3661,7 @@ "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -3702,20 +3708,20 @@ "i18n", "zf" ], - "time": "2018-05-16T16:39:13+00:00" + "time": "2019-09-30T12:04:37+00:00" }, { "name": "zendframework/zend-inputfilter", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-inputfilter.git", - "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c" + "reference": "1f44a2e9bc394a71638b43bc7024b572fa65410e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", - "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", + "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/1f44a2e9bc394a71638b43bc7024b572fa65410e", + "reference": "1f44a2e9bc394a71638b43bc7024b572fa65410e", "shasum": "" }, "require": { @@ -3726,7 +3732,7 @@ "zendframework/zend-validator": "^2.11" }, "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.15", "psr/http-message": "^1.0", "zendframework/zend-coding-standard": "~1.0.0" }, @@ -3759,7 +3765,7 @@ "inputfilter", "zf" ], - "time": "2019-01-30T16:58:51+00:00" + "time": "2019-08-28T19:45:32+00:00" }, { "name": "zendframework/zend-json", @@ -3818,16 +3824,16 @@ }, { "name": "zendframework/zend-loader", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-loader.git", - "reference": "78f11749ea340f6ca316bca5958eef80b38f9b6c" + "reference": "91da574d29b58547385b2298c020b257310898c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/78f11749ea340f6ca316bca5958eef80b38f9b6c", - "reference": "78f11749ea340f6ca316bca5958eef80b38f9b6c", + "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/91da574d29b58547385b2298c020b257310898c6", + "reference": "91da574d29b58547385b2298c020b257310898c6", "shasum": "" }, "require": { @@ -3859,20 +3865,20 @@ "loader", "zf" ], - "time": "2018-04-30T15:20:54+00:00" + "time": "2019-09-04T19:38:14+00:00" }, { "name": "zendframework/zend-log", - "version": "2.10.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-log.git", - "reference": "9cec3b092acb39963659c2f32441cccc56b3f430" + "reference": "cb278772afdacb1924342248a069330977625ae6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-log/zipball/9cec3b092acb39963659c2f32441cccc56b3f430", - "reference": "9cec3b092acb39963659c2f32441cccc56b3f430", + "url": "https://api.github.com/repos/zendframework/zend-log/zipball/cb278772afdacb1924342248a069330977625ae6", + "reference": "cb278772afdacb1924342248a069330977625ae6", "shasum": "" }, "require": { @@ -3885,8 +3891,8 @@ "psr/log-implementation": "1.0.0" }, "require-dev": { - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^5.7.15 || ^6.0.8", + "mikey179/vfsstream": "^1.6.7", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.15", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-db": "^2.6", "zendframework/zend-escaper": "^2.5", @@ -3897,7 +3903,6 @@ "suggest": { "ext-mongo": "mongo extension to use Mongo writer", "ext-mongodb": "mongodb extension to use MongoDB writer", - "zendframework/zend-console": "Zend\\Console component to use the RequestID log processor", "zendframework/zend-db": "Zend\\Db component to use the database log writer", "zendframework/zend-escaper": "Zend\\Escaper component, for use in the XML log formatter", "zendframework/zend-mail": "Zend\\Mail component to use the email log writer", @@ -3906,8 +3911,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" }, "zf": { "component": "Zend\\Log", @@ -3923,14 +3928,14 @@ "license": [ "BSD-3-Clause" ], - "description": "component for general purpose logging", - "homepage": "https://github.com/zendframework/zend-log", + "description": "Robust, composite logger with filtering, formatting, and PSR-3 support", "keywords": [ + "ZendFramework", "log", "logging", - "zf2" + "zf" ], - "time": "2018-04-09T21:59:51+00:00" + "time": "2019-08-23T21:28:18+00:00" }, { "name": "zendframework/zend-mail", @@ -4046,16 +4051,16 @@ }, { "name": "zendframework/zend-mime", - "version": "2.7.1", + "version": "2.7.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-mime.git", - "reference": "52ae5fa9f12845cae749271034a2d594f0e4c6f2" + "reference": "c91e0350be53cc9d29be15563445eec3b269d7c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-mime/zipball/52ae5fa9f12845cae749271034a2d594f0e4c6f2", - "reference": "52ae5fa9f12845cae749271034a2d594f0e4c6f2", + "url": "https://api.github.com/repos/zendframework/zend-mime/zipball/c91e0350be53cc9d29be15563445eec3b269d7c1", + "reference": "c91e0350be53cc9d29be15563445eec3b269d7c1", "shasum": "" }, "require": { @@ -4073,8 +4078,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "autoload": { @@ -4087,26 +4092,25 @@ "BSD-3-Clause" ], "description": "Create and parse MIME messages and parts", - "homepage": "https://github.com/zendframework/zend-mime", "keywords": [ "ZendFramework", "mime", "zf" ], - "time": "2018-05-14T19:02:50+00:00" + "time": "2019-10-16T19:30:37+00:00" }, { "name": "zendframework/zend-modulemanager", - "version": "2.8.2", + "version": "2.8.4", "source": { "type": "git", "url": "https://github.com/zendframework/zend-modulemanager.git", - "reference": "394df6e12248ac430a312d4693f793ee7120baa6" + "reference": "b2596d24b9a4e36a3cd114d35d3ad0918db9a243" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-modulemanager/zipball/394df6e12248ac430a312d4693f793ee7120baa6", - "reference": "394df6e12248ac430a312d4693f793ee7120baa6", + "url": "https://api.github.com/repos/zendframework/zend-modulemanager/zipball/b2596d24b9a4e36a3cd114d35d3ad0918db9a243", + "reference": "b2596d24b9a4e36a3cd114d35d3ad0918db9a243", "shasum": "" }, "require": { @@ -4116,7 +4120,7 @@ "zendframework/zend-stdlib": "^3.1 || ^2.7" }, "require-dev": { - "phpunit/phpunit": "^6.0.8 || ^5.7.15", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-console": "^2.6", "zendframework/zend-di": "^2.6", @@ -4133,8 +4137,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.8.x-dev", + "dev-develop": "2.9.x-dev" } }, "autoload": { @@ -4147,13 +4151,12 @@ "BSD-3-Clause" ], "description": "Modular application system for zend-mvc applications", - "homepage": "https://github.com/zendframework/zend-modulemanager", "keywords": [ "ZendFramework", "modulemanager", "zf" ], - "time": "2017-12-02T06:11:18+00:00" + "time": "2019-10-28T13:29:38+00:00" }, { "name": "zendframework/zend-mvc", @@ -4301,16 +4304,16 @@ }, { "name": "zendframework/zend-serializer", - "version": "2.9.0", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-serializer.git", - "reference": "0172690db48d8935edaf625c4cba38b79719892c" + "reference": "6fb7ae016cfdf0cfcdfa2b989e6a65f351170e21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/0172690db48d8935edaf625c4cba38b79719892c", - "reference": "0172690db48d8935edaf625c4cba38b79719892c", + "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/6fb7ae016cfdf0cfcdfa2b989e6a65f351170e21", + "reference": "6fb7ae016cfdf0cfcdfa2b989e6a65f351170e21", "shasum": "" }, "require": { @@ -4319,7 +4322,7 @@ "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.25 || ^6.4.4", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-math": "^2.6 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" @@ -4348,26 +4351,26 @@ "license": [ "BSD-3-Clause" ], - "description": "provides an adapter based interface to simply generate storable representation of PHP types by different facilities, and recover", + "description": "Serialize and deserialize PHP structures to a variety of representations", "keywords": [ "ZendFramework", "serializer", "zf" ], - "time": "2018-05-14T18:45:18+00:00" + "time": "2019-10-19T08:06:30+00:00" }, { "name": "zendframework/zend-server", - "version": "2.8.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-server.git", - "reference": "23a2e9a5599c83c05da831cb7c649e8a7809595e" + "reference": "d80c44700ebb92191dd9a3005316a6ab6637c0d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-server/zipball/23a2e9a5599c83c05da831cb7c649e8a7809595e", - "reference": "23a2e9a5599c83c05da831cb7c649e8a7809595e", + "url": "https://api.github.com/repos/zendframework/zend-server/zipball/d80c44700ebb92191dd9a3005316a6ab6637c0d1", + "reference": "d80c44700ebb92191dd9a3005316a6ab6637c0d1", "shasum": "" }, "require": { @@ -4401,7 +4404,7 @@ "server", "zf" ], - "time": "2018-04-30T22:21:28+00:00" + "time": "2019-10-16T18:27:05+00:00" }, { "name": "zendframework/zend-servicemanager", @@ -4457,28 +4460,28 @@ }, { "name": "zendframework/zend-session", - "version": "2.8.5", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-session.git", - "reference": "2cfd90e1a2f6b066b9f908599251d8f64f07021b" + "reference": "c289c4d733ec23a389e25c7c451f4d062088511f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-session/zipball/2cfd90e1a2f6b066b9f908599251d8f64f07021b", - "reference": "2cfd90e1a2f6b066b9f908599251d8f64f07021b", + "url": "https://api.github.com/repos/zendframework/zend-session/zipball/c289c4d733ec23a389e25c7c451f4d062088511f", + "reference": "c289c4d733ec23a389e25c7c451f4d062088511f", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "zendframework/zend-stdlib": "^3.2.1" }, "require-dev": { "container-interop/container-interop": "^1.1", "mongodb/mongodb": "^1.0.1", "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", - "phpunit/phpunit": "^5.7.5 || >=6.0.13 <6.5.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-db": "^2.7", @@ -4497,8 +4500,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev", - "dev-develop": "2.9-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Session", @@ -4514,13 +4517,13 @@ "license": [ "BSD-3-Clause" ], - "description": "manage and preserve session data, a logical complement of cookie data, across multiple page requests by the same client", + "description": "Object-oriented interface to PHP sessions and storage", "keywords": [ "ZendFramework", "session", "zf" ], - "time": "2018-02-22T16:33:54+00:00" + "time": "2019-10-28T19:40:43+00:00" }, { "name": "zendframework/zend-soap", @@ -4623,16 +4626,16 @@ }, { "name": "zendframework/zend-text", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-text.git", - "reference": "ca987dd4594f5f9508771fccd82c89bc7fbb39ac" + "reference": "41e32dafa4015e160e2f95a7039554385c71624d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-text/zipball/ca987dd4594f5f9508771fccd82c89bc7fbb39ac", - "reference": "ca987dd4594f5f9508771fccd82c89bc7fbb39ac", + "url": "https://api.github.com/repos/zendframework/zend-text/zipball/41e32dafa4015e160e2f95a7039554385c71624d", + "reference": "41e32dafa4015e160e2f95a7039554385c71624d", "shasum": "" }, "require": { @@ -4667,20 +4670,20 @@ "text", "zf" ], - "time": "2018-04-30T14:55:10+00:00" + "time": "2019-10-16T20:36:27+00:00" }, { "name": "zendframework/zend-uri", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "b2785cd38fe379a784645449db86f21b7739b1ee" + "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/b2785cd38fe379a784645449db86f21b7739b1ee", - "reference": "b2785cd38fe379a784645449db86f21b7739b1ee", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/bfc4a5b9a309711e968d7c72afae4ac50c650083", + "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083", "shasum": "" }, "require": { @@ -4714,20 +4717,20 @@ "uri", "zf" ], - "time": "2019-02-27T21:39:04+00:00" + "time": "2019-10-07T13:35:33+00:00" }, { "name": "zendframework/zend-validator", - "version": "2.12.0", + "version": "2.12.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "64c33668e5fa2d39c6289a878f927ea2b0850c30" + "reference": "fd24920c2afcf2a70d11f67c3457f8f509453a62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/64c33668e5fa2d39c6289a878f927ea2b0850c30", - "reference": "64c33668e5fa2d39c6289a878f927ea2b0850c30", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/fd24920c2afcf2a70d11f67c3457f8f509453a62", + "reference": "fd24920c2afcf2a70d11f67c3457f8f509453a62", "shasum": "" }, "require": { @@ -4781,26 +4784,26 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a set of commonly needed validators", - "homepage": "https://github.com/zendframework/zend-validator", + "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", "keywords": [ + "ZendFramework", "validator", - "zf2" + "zf" ], - "time": "2019-01-30T14:26:10+00:00" + "time": "2019-10-29T08:33:25+00:00" }, { "name": "zendframework/zend-view", - "version": "2.11.2", + "version": "2.11.3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-view.git", - "reference": "4f5cb653ed4c64bb8d9bf05b294300feb00c67f2" + "reference": "e766457bd6ce13c5354e443bb949511b6904d7f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-view/zipball/4f5cb653ed4c64bb8d9bf05b294300feb00c67f2", - "reference": "4f5cb653ed4c64bb8d9bf05b294300feb00c67f2", + "url": "https://api.github.com/repos/zendframework/zend-view/zipball/e766457bd6ce13c5354e443bb949511b6904d7f5", + "reference": "e766457bd6ce13c5354e443bb949511b6904d7f5", "shasum": "" }, "require": { @@ -4868,13 +4871,13 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a system of helpers, output filters, and variable escaping", - "homepage": "https://github.com/zendframework/zend-view", + "description": "Flexible view layer supporting and providing multiple view layers, helpers, and more", "keywords": [ + "ZendFramework", "view", - "zf2" + "zf" ], - "time": "2019-02-19T17:40:15+00:00" + "time": "2019-10-11T21:10:04+00:00" } ], "packages-dev": [ @@ -4931,25 +4934,26 @@ }, { "name": "allure-framework/allure-php-api", - "version": "1.1.4", + "version": "1.1.5", "source": { "type": "git", - "url": "https://github.com/allure-framework/allure-php-adapter-api.git", - "reference": "a462a0da121681577033e13c123b6cc4e89cdc64" + "url": "https://github.com/allure-framework/allure-php-commons.git", + "reference": "c7a675823ad75b8e02ddc364baae21668e7c4e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-php-adapter-api/zipball/a462a0da121681577033e13c123b6cc4e89cdc64", - "reference": "a462a0da121681577033e13c123b6cc4e89cdc64", + "url": "https://api.github.com/repos/allure-framework/allure-php-commons/zipball/c7a675823ad75b8e02ddc364baae21668e7c4e88", + "reference": "c7a675823ad75b8e02ddc364baae21668e7c4e88", "shasum": "" }, "require": { - "jms/serializer": ">=0.16.0", - "moontoast/math": ">=1.1.0", + "jms/serializer": "^0.16.0", "php": ">=5.4.0", - "phpunit/phpunit": ">=4.0.0", - "ramsey/uuid": ">=3.0.0", - "symfony/http-foundation": ">=2.0" + "ramsey/uuid": "^3.0.0", + "symfony/http-foundation": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0.0" }, "type": "library", "autoload": { @@ -4967,8 +4971,8 @@ "authors": [ { "name": "Ivan Krutov", - "role": "Developer", - "email": "vania-pooh@yandex-team.ru" + "email": "vania-pooh@yandex-team.ru", + "role": "Developer" } ], "description": "PHP API for Allure adapter", @@ -4979,7 +4983,7 @@ "php", "report" ], - "time": "2016-12-07T12:15:46+00:00" + "time": "2018-05-25T14:02:11+00:00" }, { "name": "allure-framework/allure-phpunit", @@ -5090,6 +5094,99 @@ ], "time": "2019-01-16T14:22:17+00:00" }, + { + "name": "cache/cache", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/cache.git", + "reference": "902b2e5b54ea57e3a801437748652228c4c58604" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/cache/zipball/902b2e5b54ea57e3a801437748652228c4c58604", + "reference": "902b2e5b54ea57e3a801437748652228c4c58604", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.3", + "league/flysystem": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/log": "^1.0", + "psr/simple-cache": "^1.0" + }, + "conflict": { + "cache/adapter-common": "*", + "cache/apc-adapter": "*", + "cache/apcu-adapter": "*", + "cache/array-adapter": "*", + "cache/chain-adapter": "*", + "cache/doctrine-adapter": "*", + "cache/filesystem-adapter": "*", + "cache/hierarchical-cache": "*", + "cache/illuminate-adapter": "*", + "cache/memcache-adapter": "*", + "cache/memcached-adapter": "*", + "cache/mongodb-adapter": "*", + "cache/predis-adapter": "*", + "cache/psr-6-doctrine-bridge": "*", + "cache/redis-adapter": "*", + "cache/session-handler": "*", + "cache/taggable-cache": "*", + "cache/void-adapter": "*" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "defuse/php-encryption": "^2.0", + "illuminate/cache": "^5.4", + "mockery/mockery": "^0.9", + "phpunit/phpunit": "^4.0 || ^5.1", + "predis/predis": "^1.0", + "symfony/cache": "dev-master" + }, + "suggest": { + "ext-apc": "APC extension is required to use the APC Adapter", + "ext-apcu": "APCu extension is required to use the APCu Adapter", + "ext-memcache": "Memcache extension is required to use the Memcache Adapter", + "ext-memcached": "Memcached extension is required to use the Memcached Adapter", + "ext-mongodb": "Mongodb extension required to use the Mongodb adapter", + "ext-redis": "Redis extension is required to use the Redis adapter", + "mongodb/mongodb": "Mongodb lib required to use the Mongodb adapter" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cache\\": "src/" + }, + "exclude-from-classmap": [ + "**/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "Library of all the php-cache adapters", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "psr6" + ], + "time": "2017-03-28T16:08:48+00:00" + }, { "name": "codeception/codeception", "version": "2.4.5", @@ -5183,16 +5280,16 @@ }, { "name": "codeception/phpunit-wrapper", - "version": "6.6.1", + "version": "6.7.0", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c" + "reference": "93f59e028826464eac086052fa226e58967f6907" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", - "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/93f59e028826464eac086052fa226e58967f6907", + "reference": "93f59e028826464eac086052fa226e58967f6907", "shasum": "" }, "require": { @@ -5225,7 +5322,7 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2019-02-26T20:47:39+00:00" + "time": "2019-08-18T15:43:35+00:00" }, { "name": "codeception/stub", @@ -5627,20 +5724,20 @@ }, { "name": "consolidation/robo", - "version": "1.4.10", + "version": "1.4.11", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "e5a6ca64cf1324151873672e484aceb21f365681" + "reference": "5fa1d901776a628167a325baa9db95d8edf13a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/e5a6ca64cf1324151873672e484aceb21f365681", - "reference": "e5a6ca64cf1324151873672e484aceb21f365681", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/5fa1d901776a628167a325baa9db95d8edf13a80", + "reference": "5fa1d901776a628167a325baa9db95d8edf13a80", "shasum": "" }, "require": { - "consolidation/annotated-command": "^2.10.2", + "consolidation/annotated-command": "^2.11.0", "consolidation/config": "^1.2", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", @@ -5670,6 +5767,7 @@ "pear/archive_tar": "^1.4.4", "php-coveralls/php-coveralls": "^1", "phpunit/php-code-coverage": "~2|~4", + "sebastian/comparator": "^1.2.4", "squizlabs/php_codesniffer": "^2.8" }, "suggest": { @@ -5731,7 +5829,7 @@ } ], "description": "Modern task runner", - "time": "2019-07-29T15:40:50+00:00" + "time": "2019-10-29T15:50:02+00:00" }, { "name": "consolidation/self-update", @@ -5783,6 +5881,158 @@ "description": "Provides a self:update command for Symfony Console applications.", "time": "2018-10-28T01:52:03+00:00" }, + { + "name": "csharpru/vault-php", + "version": "3.5.3", + "source": { + "type": "git", + "url": "https://github.com/CSharpRU/vault-php.git", + "reference": "04be9776310fe7d1afb97795645f95c21e6b4fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CSharpRU/vault-php/zipball/04be9776310fe7d1afb97795645f95c21e6b4fcf", + "reference": "04be9776310fe7d1afb97795645f95c21e6b4fcf", + "shasum": "" + }, + "require": { + "cache/cache": "^0.4.0", + "doctrine/inflector": "~1.1.0", + "guzzlehttp/promises": "^1.3", + "guzzlehttp/psr7": "^1.4", + "psr/cache": "^1.0", + "psr/log": "^1.0", + "weew/helpers-array": "^1.3" + }, + "require-dev": { + "codacy/coverage": "^1.1", + "codeception/codeception": "^2.2", + "csharpru/vault-php-guzzle6-transport": "~2.0", + "php-vcr/php-vcr": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Vault\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Yaroslav Lukyanov", + "email": "c_sharp@mail.ru" + } + ], + "description": "Best Vault client for PHP that you can find", + "time": "2018-04-28T04:52:17+00:00" + }, + { + "name": "csharpru/vault-php-guzzle6-transport", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/CSharpRU/vault-php-guzzle6-transport.git", + "reference": "33c392120ac9f253b62b034e0e8ffbbdb3513bd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CSharpRU/vault-php-guzzle6-transport/zipball/33c392120ac9f253b62b034e0e8ffbbdb3513bd8", + "reference": "33c392120ac9f253b62b034e0e8ffbbdb3513bd8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~6.2", + "guzzlehttp/promises": "^1.3", + "guzzlehttp/psr7": "^1.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "VaultTransports\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Yaroslav Lukyanov", + "email": "c_sharp@mail.ru" + } + ], + "description": "Guzzle6 transport for Vault PHP client", + "time": "2019-03-10T06:17:37+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.5.0", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "e749410375ff6fb7a040a68878c656c2e610b132" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e749410375ff6fb7a040a68878c656c2e610b132", + "reference": "e749410375ff6fb7a040a68878c656c2e610b132", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0", + "php": "^5.3|^7", + "squizlabs/php_codesniffer": "^2|^3" + }, + "require-dev": { + "composer/composer": "*", + "phpcompatibility/php-compatibility": "^9.0", + "sensiolabs/security-checker": "^4.1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "time": "2018-10-26T13:21:45+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v1.1.0", @@ -5844,16 +6094,16 @@ }, { "name": "doctrine/annotations", - "version": "v1.6.1", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24" + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/53120e0eb10355388d6ccbe462f1fea34ddadb24", - "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/904dca4eb10715b92569fbcd79e201d5c349b6bc", + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc", "shasum": "" }, "require": { @@ -5862,12 +6112,12 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^7.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -5880,6 +6130,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -5888,10 +6142,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -5908,40 +6158,120 @@ "docblock", "parser" ], - "time": "2019-03-25T19:12:02+00:00" + "time": "2019-10-01T18:55:10+00:00" }, { - "name": "doctrine/collections", - "version": "v1.6.2", + "name": "doctrine/cache", + "version": "1.9.1", "source": { "type": "git", - "url": "https://github.com/doctrine/collections.git", - "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be" + "url": "https://github.com/doctrine/cache.git", + "reference": "89a5c76c39c292f7798f964ab3c836c3f8192a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/c5e0bc17b1620e97c968ac409acbff28b8b850be", - "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be", + "url": "https://api.github.com/repos/doctrine/cache/zipball/89a5c76c39c292f7798f964ab3c836c3f8192a55", + "reference": "89a5c76c39c292f7798f964ab3c836c3f8192a55", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "~7.1" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" }, "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", "doctrine/coding-standard": "^6.0", - "phpstan/phpstan-shim": "^0.9.2", + "mongodb/mongodb": "^1.1", "phpunit/phpunit": "^7.0", - "vimeo/psalm": "^3.2.2" + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.9.x-dev" } }, "autoload": { "psr-4": { - "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "riak", + "xcache" + ], + "time": "2019-11-15T14:31:57+00:00" + }, + { + "name": "doctrine/inflector", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "90b2128806bfde671b6952ab8bea493942c1fdae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae", + "reference": "90b2128806bfde671b6952ab8bea493942c1fdae", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Inflector\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5970,28 +6300,28 @@ "email": "schmittjoh@gmail.com" } ], - "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", - "homepage": "https://www.doctrine-project.org/projects/collections.html", + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", "keywords": [ - "array", - "collections", - "iterators", - "php" + "inflection", + "pluralize", + "singularize", + "string" ], - "time": "2019-06-09T13:48:14+00:00" + "time": "2015-11-06T14:35:42+00:00" }, { "name": "doctrine/instantiator", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "a2c590166b2133a4633738648b6b064edae0814a" + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", - "reference": "a2c590166b2133a4633738648b6b064edae0814a", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", "shasum": "" }, "require": { @@ -6034,32 +6364,34 @@ "constructor", "instantiate" ], - "time": "2019-03-17T17:37:11+00:00" + "time": "2019-10-21T16:45:58+00:00" }, { "name": "doctrine/lexer", - "version": "1.0.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8" + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8", - "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.2" }, "require-dev": { - "phpunit/phpunit": "^4.5" + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -6072,14 +6404,14 @@ "MIT" ], "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, { "name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com" }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com" @@ -6094,53 +6426,7 @@ "parser", "php" ], - "time": "2019-06-08T11:03:04+00:00" - }, - { - "name": "epfremme/swagger-php", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/epfremmer/swagger-php.git", - "reference": "eee28a442b7e6220391ec953d3c9b936354f23bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/epfremmer/swagger-php/zipball/eee28a442b7e6220391ec953d3c9b936354f23bc", - "reference": "eee28a442b7e6220391ec953d3c9b936354f23bc", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.2", - "doctrine/collections": "^1.3", - "jms/serializer": "^1.1", - "php": ">=5.5", - "phpoption/phpoption": "^1.1", - "symfony/yaml": "^2.7|^3.1" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "~4.8|~5.0", - "satooshi/php-coveralls": "^1.0" - }, - "type": "package", - "autoload": { - "psr-4": { - "Epfremme\\Swagger\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Edward Pfremmer", - "email": "epfremme@nerdery.com" - } - ], - "description": "Library for parsing swagger documentation into PHP entities for use in testing and code generation", - "time": "2016-09-26T17:24:17+00:00" + "time": "2019-10-30T14:39:59+00:00" }, { "name": "facebook/webdriver", @@ -6245,16 +6531,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.14.4", + "version": "v2.14.6", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "69ccf81f3c968be18d646918db94ab88ddf3594f" + "reference": "8d18a8bb180e2acde1c8031db09aefb9b73f6127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/69ccf81f3c968be18d646918db94ab88ddf3594f", - "reference": "69ccf81f3c968be18d646918db94ab88ddf3594f", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/8d18a8bb180e2acde1c8031db09aefb9b73f6127", + "reference": "8d18a8bb180e2acde1c8031db09aefb9b73f6127", "shasum": "" }, "require": { @@ -6284,9 +6570,10 @@ "php-cs-fixer/accessible-object": "^1.0", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", - "symfony/phpunit-bridge": "^4.3" + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", + "phpunitgoodpractices/traits": "^1.5.1", + "symfony/phpunit-bridge": "^4.0", + "symfony/yaml": "^3.0 || ^4.0" }, "suggest": { "ext-mbstring": "For handling non-UTF8 characters in cache signature.", @@ -6319,30 +6606,30 @@ "MIT" ], "authors": [ - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" } ], "description": "A tool to automatically fix PHP code style", - "time": "2019-06-01T10:29:34+00:00" + "time": "2019-08-31T12:47:52+00:00" }, { "name": "fzaninotto/faker", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/fzaninotto/Faker.git", - "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de" + "reference": "27a216cbe72327b2d6369fab721a5843be71e57d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/f72816b43e74063c8b10357394b6bba8cb1c10de", - "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/27a216cbe72327b2d6369fab721a5843be71e57d", + "reference": "27a216cbe72327b2d6369fab721a5843be71e57d", "shasum": "" }, "require": { @@ -6351,13 +6638,11 @@ "require-dev": { "ext-intl": "*", "phpunit/phpunit": "^4.8.35 || ^5.7", - "squizlabs/php_codesniffer": "^1.5" + "squizlabs/php_codesniffer": "^2.9.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } + "branch-alias": [] }, "autoload": { "psr-4": { @@ -6379,7 +6664,7 @@ "faker", "fixtures" ], - "time": "2018-07-12T10:23:15+00:00" + "time": "2019-11-14T13:13:06+00:00" }, { "name": "grasmash/expander", @@ -6476,6 +6761,48 @@ "description": "Expands internal property references in a yaml file.", "time": "2017-12-16T16:06:03+00:00" }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "time": "2014-11-20T16:49:30+00:00" + }, { "name": "jms/metadata", "version": "1.7.0", @@ -6568,56 +6895,44 @@ }, { "name": "jms/serializer", - "version": "1.14.0", + "version": "0.16.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "ee96d57024af9a7716d56fcbe3aa94b3d030f3ca" + "reference": "c8a171357ca92b6706e395c757f334902d430ea9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/ee96d57024af9a7716d56fcbe3aa94b3d030f3ca", - "reference": "ee96d57024af9a7716d56fcbe3aa94b3d030f3ca", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/c8a171357ca92b6706e395c757f334902d430ea9", + "reference": "c8a171357ca92b6706e395c757f334902d430ea9", "shasum": "" }, "require": { - "doctrine/annotations": "^1.0", - "doctrine/instantiator": "^1.0.3", - "jms/metadata": "^1.3", + "doctrine/annotations": "1.*", + "jms/metadata": "~1.1", "jms/parser-lib": "1.*", - "php": "^5.5|^7.0", - "phpcollection/phpcollection": "~0.1", - "phpoption/phpoption": "^1.1" - }, - "conflict": { - "twig/twig": "<1.12" + "php": ">=5.3.2", + "phpcollection/phpcollection": "~0.1" }, "require-dev": { "doctrine/orm": "~2.1", - "doctrine/phpcr-odm": "^1.3|^2.0", - "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", - "phpunit/phpunit": "^4.8|^5.0", + "doctrine/phpcr-odm": "~1.0.1", + "jackalope/jackalope-doctrine-dbal": "1.0.*", "propel/propel1": "~1.7", - "psr/container": "^1.0", - "symfony/dependency-injection": "^2.7|^3.3|^4.0", - "symfony/expression-language": "^2.6|^3.0", - "symfony/filesystem": "^2.1", - "symfony/form": "~2.1|^3.0", - "symfony/translation": "^2.1|^3.0", - "symfony/validator": "^2.2|^3.0", - "symfony/yaml": "^2.1|^3.0", - "twig/twig": "~1.12|~2.0" + "symfony/filesystem": "2.*", + "symfony/form": "~2.1", + "symfony/translation": "~2.0", + "symfony/validator": "~2.0", + "symfony/yaml": "2.*", + "twig/twig": ">=1.8,<2.0-dev" }, "suggest": { - "doctrine/cache": "Required if you like to use cache functionality.", - "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", "symfony/yaml": "Required if you'd like to serialize data to YAML format." }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.14-dev" + "dev-master": "0.15-dev" } }, "autoload": { @@ -6627,16 +6942,14 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache2" ], "authors": [ { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - }, - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh", + "role": "Developer of wrapped JMSSerializerBundle" } ], "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", @@ -6648,7 +6961,7 @@ "serialization", "xml" ], - "time": "2019-04-17T08:12:16+00:00" + "time": "2014-03-18T08:39:00+00:00" }, { "name": "league/container", @@ -6715,6 +7028,90 @@ ], "time": "2017-05-10T09:20:27+00:00" }, + { + "name": "league/flysystem", + "version": "1.0.57", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a", + "reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.10" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2019-10-16T21:01:05+00:00" + }, { "name": "lusitanian/oauth", "version": "v0.8.11", @@ -6784,52 +7181,64 @@ }, { "name": "magento/magento-coding-standard", - "version": "4", + "version": "5", "source": { "type": "git", "url": "https://github.com/magento/magento-coding-standard.git", - "reference": "d24e0230a532e19941ed264f57db38fad5b1008a" + "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/d24e0230a532e19941ed264f57db38fad5b1008a", - "reference": "d24e0230a532e19941ed264f57db38fad5b1008a", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/da46c5d57a43c950dfa364edc7f1f0436d5353a5", + "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5", "shasum": "" }, "require": { "php": ">=5.6.0", - "squizlabs/php_codesniffer": "^3.4" + "squizlabs/php_codesniffer": "^3.4", + "webonyx/graphql-php": ">=0.12.6 <1.0" }, "require-dev": { "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "phpcodesniffer-standard", + "autoload": { + "classmap": [ + "PHP_CodeSniffer/Tokenizers/" + ], + "psr-4": { + "Magento2\\": "Magento2/" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "OSL-3.0", "AFL-3.0" ], "description": "A set of Magento specific PHP CodeSniffer rules.", - "time": "2019-08-06T15:58:37+00:00" + "time": "2019-11-04T22:08:27+00:00" }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.4.3", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "9e9a20fd4c77833ef41ac07eb076a7f2434ce61c" + "reference": "f627085a469da79e4a628d4bf0452f12aefa4389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/9e9a20fd4c77833ef41ac07eb076a7f2434ce61c", - "reference": "9e9a20fd4c77833ef41ac07eb076a7f2434ce61c", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/f627085a469da79e4a628d4bf0452f12aefa4389", + "reference": "f627085a469da79e4a628d4bf0452f12aefa4389", "shasum": "" }, "require": { "allure-framework/allure-codeception": "~1.3.0", "codeception/codeception": "~2.3.4 || ~2.4.0 ", + "composer/composer": "^1.4", "consolidation/robo": "^1.0.0", + "csharpru/vault-php": "~3.5.3", + "csharpru/vault-php-guzzle6-transport": "^2.0", "ext-curl": "*", "flow/jsonpath": ">0.2", "fzaninotto/faker": "^1.6", @@ -6885,27 +7294,27 @@ "magento", "testing" ], - "time": "2019-08-02T14:26:18+00:00" + "time": "2019-10-31T14:52:02+00:00" }, { "name": "mikey179/vfsstream", - "version": "v1.6.6", + "version": "v1.6.8", "source": { "type": "git", "url": "https://github.com/bovigo/vfsStream.git", - "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d" + "reference": "231c73783ebb7dd9ec77916c10037eff5a2b6efe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/095238a0711c974ae5b4ebf4c4534a23f3f6c99d", - "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/231c73783ebb7dd9ec77916c10037eff5a2b6efe", + "reference": "231c73783ebb7dd9ec77916c10037eff5a2b6efe", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "~4.5" + "phpunit/phpunit": "^4.5|^5.0" }, "type": "library", "extra": { @@ -6925,62 +7334,13 @@ "authors": [ { "name": "Frank Kleine", - "role": "Developer", - "homepage": "http://frankkleine.de/" + "homepage": "http://frankkleine.de/", + "role": "Developer" } ], "description": "Virtual file system to mock the real file system in unit tests.", "homepage": "http://vfs.bovigo.org/", - "time": "2019-04-08T13:54:32+00:00" - }, - { - "name": "moontoast/math", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/ramsey/moontoast-math.git", - "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/moontoast-math/zipball/c2792a25df5cad4ff3d760dd37078fc5b6fccc79", - "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79", - "shasum": "" - }, - "require": { - "ext-bcmath": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "jakub-onderka/php-parallel-lint": "^0.9.0", - "phpunit/phpunit": "^4.7|>=5.0 <5.4", - "satooshi/php-coveralls": "^0.6.1", - "squizlabs/php_codesniffer": "^2.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Moontoast\\Math\\": "src/Moontoast/Math/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - } - ], - "description": "A mathematics library, providing functionality for large numbers", - "homepage": "https://github.com/ramsey/moontoast-math", - "keywords": [ - "bcmath", - "math" - ], - "time": "2017-02-16T16:54:46+00:00" + "time": "2019-10-30T15:31:00+00:00" }, { "name": "mustache/mustache", @@ -7030,16 +7390,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.9.1", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72" + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", "shasum": "" }, "require": { @@ -7074,7 +7434,7 @@ "object", "object graph" ], - "time": "2019-04-07T13:18:21+00:00" + "time": "2019-08-09T12:45:53+00:00" }, { "name": "pdepend/pdepend", @@ -7317,37 +7677,93 @@ ], "time": "2015-05-17T12:39:23+00:00" }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "1f37659196e4f3113ea506a7efba201c52303bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/1f37659196e4f3113ea506a7efba201c52303bf1", + "reference": "1f37659196e4f3113ea506a7efba201c52303bf1", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "time": "2019-11-15T04:12:02+00:00" + }, { "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "~6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7369,30 +7785,30 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2018-08-07T13:53:10+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.1", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c" + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", - "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", "webmozart/assert": "^1.0" }, "require-dev": { - "doctrine/instantiator": "~1.0.5", + "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", "phpunit/phpunit": "^6.4" }, @@ -7420,41 +7836,40 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2019-04-30T17:48:53+00:00" + "time": "2019-09-12T14:27:41+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -7467,7 +7882,8 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" }, { "name": "phpmd/phpmd", @@ -7539,28 +7955,28 @@ }, { "name": "phpoption/phpoption", - "version": "1.5.0", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" + "reference": "2ba2586380f8d2b44ad1b9feb61c371020b27793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", - "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/2ba2586380f8d2b44ad1b9feb61c371020b27793", + "reference": "2ba2586380f8d2b44ad1b9feb61c371020b27793", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "4.7.*" + "phpunit/phpunit": "^4.7|^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -7570,7 +7986,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache2" + "Apache-2.0" ], "authors": [ { @@ -7585,26 +8001,26 @@ "php", "type" ], - "time": "2015-07-25T16:39:46+00:00" + "time": "2019-11-06T22:27:00+00:00" }, { "name": "phpspec/prophecy", - "version": "1.8.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, @@ -7648,7 +8064,7 @@ "spy", "stub" ], - "time": "2019-06-13T12:50:23+00:00" + "time": "2019-10-03T11:07:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8043,6 +8459,100 @@ "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.1", @@ -8256,16 +8766,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.0", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { @@ -8292,6 +8802,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -8300,17 +8814,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -8319,7 +8829,7 @@ "export", "exporter" ], - "time": "2017-04-03T13:19:02+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/finder-facade", @@ -8744,16 +9254,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca" + "reference": "b14fa08508afd152257d5dcc7adb5f278654d972" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a29dd02a1f3f81b9a15c7730cc3226718ddb55ca", - "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/b14fa08508afd152257d5dcc7adb5f278654d972", + "reference": "b14fa08508afd152257d5dcc7adb5f278654d972", "shasum": "" }, "require": { @@ -8799,20 +9309,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2019-06-11T15:41:59+00:00" + "time": "2019-10-28T17:07:32+00:00" }, { "name": "symfony/config", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "a17a2aea43950ce83a0603ed301bac362eb86870" + "reference": "8267214841c44d315a55242ea867684eb43c42ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/a17a2aea43950ce83a0603ed301bac362eb86870", - "reference": "a17a2aea43950ce83a0603ed301bac362eb86870", + "url": "https://api.github.com/repos/symfony/config/zipball/8267214841c44d315a55242ea867684eb43c42ce", + "reference": "8267214841c44d315a55242ea867684eb43c42ce", "shasum": "" }, "require": { @@ -8863,26 +9373,26 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2019-07-18T10:34:59+00:00" + "time": "2019-11-08T08:31:27+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "9ad1b83d474ae17156f6914cb81ffe77aeac3a9b" + "reference": "80c6d9e19467dfbba14f830ed478eb592ce51b64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9ad1b83d474ae17156f6914cb81ffe77aeac3a9b", - "reference": "9ad1b83d474ae17156f6914cb81ffe77aeac3a9b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/80c6d9e19467dfbba14f830ed478eb592ce51b64", + "reference": "80c6d9e19467dfbba14f830ed478eb592ce51b64", "shasum": "" }, "require": { "php": "^7.1.3", "psr/container": "^1.0", - "symfony/service-contracts": "^1.1.2" + "symfony/service-contracts": "^1.1.6" }, "conflict": { "symfony/config": "<4.3", @@ -8936,20 +9446,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2019-07-26T07:03:43+00:00" + "time": "2019-11-08T16:22:27+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "291397232a2eefb3347eaab9170409981eaad0e2" + "reference": "4b9efd5708c3a38593e19b6a33e40867f4f89d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291397232a2eefb3347eaab9170409981eaad0e2", - "reference": "291397232a2eefb3347eaab9170409981eaad0e2", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4b9efd5708c3a38593e19b6a33e40867f4f89d72", + "reference": "4b9efd5708c3a38593e19b6a33e40867f4f89d72", "shasum": "" }, "require": { @@ -8997,35 +9507,35 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2019-06-13T11:03:18+00:00" + "time": "2019-10-28T17:07:32+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.3.3", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8b778ee0c27731105fbf1535f51793ad1ae0ba2b" + "reference": "3929d9fe8148d17819ad0178c748b8d339420709" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8b778ee0c27731105fbf1535f51793ad1ae0ba2b", - "reference": "8b778ee0c27731105fbf1535f51793ad1ae0ba2b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3929d9fe8148d17819ad0178c748b8d339420709", + "reference": "3929d9fe8148d17819ad0178c748b8d339420709", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/mime": "^4.3", - "symfony/polyfill-mbstring": "~1.1" + "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php54": "~1.0", + "symfony/polyfill-php55": "~1.0" }, "require-dev": { - "predis/predis": "~1.0", - "symfony/expression-language": "~3.4|~4.0" + "symfony/expression-language": "~2.4|~3.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -9052,30 +9562,24 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2019-07-23T11:21:36+00:00" + "time": "2019-11-12T12:34:41+00:00" }, { - "name": "symfony/mime", - "version": "v4.3.3", + "name": "symfony/options-resolver", + "version": "v4.3.8", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "6b7148029b1dd5eda1502064f06d01357b7b2d8b" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "f46c7fc8e207bd8a2188f54f8738f232533765a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/6b7148029b1dd5eda1502064f06d01357b7b2d8b", - "reference": "6b7148029b1dd5eda1502064f06d01357b7b2d8b", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/f46c7fc8e207bd8a2188f54f8738f232533765a4", + "reference": "f46c7fc8e207bd8a2188f54f8738f232533765a4", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "require-dev": { - "egulias/email-validator": "^2.0", - "symfony/dependency-injection": "~3.4|^4.1" + "php": "^7.1.3" }, "type": "library", "extra": { @@ -9085,7 +9589,7 @@ }, "autoload": { "psr-4": { - "Symfony\\Component\\Mime\\": "" + "Symfony\\Component\\OptionsResolver\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -9105,43 +9609,47 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A library to manipulate MIME messages", + "description": "Symfony OptionsResolver Component", "homepage": "https://symfony.com", "keywords": [ - "mime", - "mime-type" + "config", + "configuration", + "options" ], - "time": "2019-07-19T16:21:19+00:00" + "time": "2019-10-28T20:59:01+00:00" }, { - "name": "symfony/options-resolver", - "version": "v4.3.3", + "name": "symfony/polyfill-php54", + "version": "v1.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "40762ead607c8f792ee4516881369ffa553fee6f" + "url": "https://github.com/symfony/polyfill-php54.git", + "reference": "a043bcced870214922fbb4bf22679d431ec0296a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/40762ead607c8f792ee4516881369ffa553fee6f", - "reference": "40762ead607c8f792ee4516881369ffa553fee6f", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/a043bcced870214922fbb4bf22679d431ec0296a", + "reference": "a043bcced870214922fbb4bf22679d431ec0296a", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "1.12-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" + "Symfony\\Polyfill\\Php54\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -9150,54 +9658,51 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony OptionsResolver Component", + "description": "Symfony polyfill backporting some PHP 5.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "config", - "configuration", - "options" + "compatibility", + "polyfill", + "portable", + "shim" ], - "time": "2019-06-13T11:01:17+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.11.0", + "name": "symfony/polyfill-php55", + "version": "v1.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af" + "url": "https://github.com/symfony/polyfill-php55.git", + "reference": "548bb39407e78e54f785b4e18c7e0d5d9e493265" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/548bb39407e78e54f785b4e18c7e0d5d9e493265", + "reference": "548bb39407e78e54f785b4e18c7e0d5d9e493265", "shasum": "" }, "require": { - "php": ">=5.3.3", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php72": "^1.9" - }, - "suggest": { - "ext-intl": "For best performance" + "ircmaxell/password-compat": "~1.0", + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.12-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" + "Symfony\\Polyfill\\Php55\\": "" }, "files": [ "bootstrap.php" @@ -9209,38 +9714,36 @@ ], "authors": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "idn", - "intl", "polyfill", "portable", "shim" ], - "time": "2019-03-04T13:44:35+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "bc4858fb611bda58719124ca079baff854149c89" + "reference": "54b4c428a0054e254223797d2713c31e08610831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89", - "reference": "bc4858fb611bda58719124ca079baff854149c89", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/54b4c428a0054e254223797d2713c31e08610831", + "reference": "54b4c428a0054e254223797d2713c31e08610831", "shasum": "" }, "require": { @@ -9250,7 +9753,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -9286,20 +9789,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" + "reference": "04ce3335667451138df4307d6a9b61565560199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", + "reference": "04ce3335667451138df4307d6a9b61565560199e", "shasum": "" }, "require": { @@ -9308,7 +9811,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -9341,20 +9844,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/service-contracts", - "version": "v1.1.5", + "version": "v1.1.8", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d" + "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d", - "reference": "f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf", + "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf", "shasum": "" }, "require": { @@ -9399,20 +9902,20 @@ "interoperability", "standards" ], - "time": "2019-06-13T11:15:36+00:00" + "time": "2019-10-14T12:27:06+00:00" }, { "name": "symfony/stopwatch", - "version": "v4.3.3", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "6b100e9309e8979cf1978ac1778eb155c1f7d93b" + "reference": "e96c259de6abcd0cead71f0bf4d730d53ee464d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/6b100e9309e8979cf1978ac1778eb155c1f7d93b", - "reference": "6b100e9309e8979cf1978ac1778eb155c1f7d93b", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e96c259de6abcd0cead71f0bf4d730d53ee464d0", + "reference": "e96c259de6abcd0cead71f0bf4d730d53ee464d0", "shasum": "" }, "require": { @@ -9449,24 +9952,24 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2019-05-27T08:16:38+00:00" + "time": "2019-11-05T14:48:09+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.30", + "version": "v4.3.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "051d045c684148060ebfc9affb7e3f5e0899d40b" + "reference": "324cf4b19c345465fad14f3602050519e09e361d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/051d045c684148060ebfc9affb7e3f5e0899d40b", - "reference": "051d045c684148060ebfc9affb7e3f5e0899d40b", + "url": "https://api.github.com/repos/symfony/yaml/zipball/324cf4b19c345465fad14f3602050519e09e361d", + "reference": "324cf4b19c345465fad14f3602050519e09e361d", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -9481,7 +9984,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -9508,7 +10011,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-07-24T13:01:31+00:00" + "time": "2019-10-30T12:58:49+00:00" }, { "name": "theseer/fdomdocument", @@ -9643,16 +10146,16 @@ }, { "name": "webmozart/assert", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", + "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", "shasum": "" }, "require": { @@ -9660,8 +10163,7 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", "extra": { @@ -9690,7 +10192,44 @@ "check", "validate" ], - "time": "2018-12-25T11:19:39+00:00" + "time": "2019-08-24T08:43:50+00:00" + }, + { + "name": "weew/helpers-array", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/weew/helpers-array.git", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/weew/helpers-array/zipball/9bff63111f9765b4277750db8d276d92b3e16ed0", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^4.7", + "satooshi/php-coveralls": "^0.6.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/array.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxim Kott", + "email": "maximkott@gmail.com" + } + ], + "description": "Useful collection of php array helpers.", + "time": "2016-07-21T11:18:01+00:00" } ], "aliases": [], @@ -9715,7 +10254,6 @@ "ext-pdo_mysql": "*", "ext-simplexml": "*", "ext-soap": "*", - "ext-spl": "*", "ext-xsl": "*", "ext-zip": "*", "lib-libxml": "*" diff --git a/dev/tests/acceptance/tests/_data/BB-Products.csv b/dev/tests/acceptance/tests/_data/BB-Products.csv new file mode 100644 index 0000000000000..7ab03fd5eaeda --- /dev/null +++ b/dev/tests/acceptance/tests/_data/BB-Products.csv @@ -0,0 +1,118 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +BB-D2010129,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Nero","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101772</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Nero,"Ventilatore Portatile Spray FunFan Nero","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Nero,Nero,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888101772",41,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888101772",,,,,,,,,, +BB-D2010130,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Bianco","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107965</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Bianco,"Ventilatore Portatile Spray FunFan Bianco","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Bianco,Bianco,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107965",741,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107965",,,,,,,,,, +BB-D2010131,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Rosso","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107972</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Rosso,"Ventilatore Portatile Spray FunFan Rosso","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Rosso,Rosso,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107972",570,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107972",,,,,,,,,, +BB-H1000163,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005922</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0592 Blu Marino,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0592 Blu Marino,CH0592 Blu Marino,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005922",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005922",,,,,,,,,, +BB-H1000162,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0596 Grigio","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005960</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0596 Grigio,"Sedia Pieghevole Campart Travel CH0596 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0596 Grigio,CH0596 Grigio,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005960",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005960",,,,,,,,,, +BB-F1520329,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005939</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0593 Blu Marino,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0593 Blu Marino,CH0593 Blu Marino,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005939",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005939",,,,,,,,,, +BB-F1520328,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005977</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0597 Grigio,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0597 Grigio,CH0597 Grigio,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005977",7,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005977",,,,,,,,,, +BB-H4502058,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Star Wars","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Star Wars""><p>I</a> fan di Star Wars non potranno fare a meno di appendere l'<strong>orologio da parete Star Wars</strong> in casa loro! Realizzato in plastica. Funziona a batterie (1 x AA, non incluse). Diametro circa: 25,5 cm. Spessore circa: 3,5 cm.</p><p> Dimenzioni per Orologio da Parete Star Wars: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 3.8 Cm</li><li>Peso: 0.287 Kg</li></ul></p><p>Codice Prodotto (EAN): 6950687214204</p>","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Star Wars""> Maggiori Informazioni</a>",0.287,1,"Taxable Goods","Catalog, Search",22.5,,,,Orologio-da-Parete-Star-Wars,"Orologio da Parete Star Wars","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica",http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=6950687214204",130,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H4502058_84713.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84711.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84710.jpg","GTIN=6950687214204",,,,,,,,,, +BB-G0500195,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Rosso","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Rosso,"Braccialetto Sportivo a LED MegaLed Rosso","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Rosso,Rosso,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-G0500196,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Verde","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Verde,"Braccialetto Sportivo a LED MegaLed Verde","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Verde,Verde,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",37,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-I2500333,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Mom's Diner","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""><p>Decora</a> la tua cucina con l'originale <strong>orologio da parete</strong> <strong>Mom's Diner</strong> in stile vintage! È realizzato in legno. Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Mom's Diner: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345052</p>","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Mom's-Diner,"Orologio da Parete Mom's Diner","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno",http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,,,,,"2016-07-21 13:05:12",,,,,,,,,,,,,,,,,"GTIN=4029811345052",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500333_88060.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88059.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88058.jpg","GTIN=4029811345052",,,,,,,,,, +BB-I2500334,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Coffee Endless Cup","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""><p>Se</a> sei un appassionato di caffè, non puoi rimanere senza l'<strong>orologio da parete Coffee Endless Cup</strong>! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Coffee Endless Cup: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345069</p>","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Coffee-Endless-Cup,"Orologio da Parete Coffee Endless Cup","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa",http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,,,,,"2016-08-30 13:41:52",,,,,,,,,,,,,,,,,"GTIN=4029811345069",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500334_88064.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88063.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88062.jpg","GTIN=4029811345069",,,,,,,,,, +BB-V0000252,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Stop!","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346196</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Stop!,"Insegna Dito Vintage Look Stop!","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Stop!,Stop!,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346196",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346196",,,,,,,,,, +BB-V0000253,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Adults Only","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346202</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Adults Only,"Insegna Dito Vintage Look Adults Only","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Adults Only,Adults Only,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346202",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346202",,,,,,,,,, +BB-V0000254,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Talk","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346318</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Talk,"Insegna Dito Vintage Look Talk","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Talk,Talk,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346318",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346318",,,,,,,,,, +BB-V0000256,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Go Left","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346325</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Go Left,"Freccia Decorativa Vintage Look Go Left","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Go Left,Go Left,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346325",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346325",,,,,,,,,, +BB-V0000257,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Exit","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346332</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Exit,"Freccia Decorativa Vintage Look Exit","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Exit,Exit,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346332",19,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346332",,,,,,,,,, +BB-V0000258,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Cold beer here","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346349</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Cold beer here,"Freccia Decorativa Vintage Look Cold beer here","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Cold beer here,Cold beer here,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346349",20,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346349",,,,,,,,,, +BB-V0200190,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Bianco","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Bianco,"Ciotola in Bambù TakeTokio Bianco","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Bianco,Bianco,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200192,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Grigio","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Grigio,"Ciotola in Bambù TakeTokio Grigio","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Grigio,Grigio,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",26,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200191,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Nero","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Nero,"Ciotola in Bambù TakeTokio Nero","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Nero,Nero,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200360,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Rosa","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Rosa,"Scatola porta Tè Flower Vintage Coconut Rosa","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Rosa,Rosa,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",13,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200361,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Azzurro","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Azzurro,"Scatola porta Tè Flower Vintage Coconut Azzurro","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Azzurro,Azzurro,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200353,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Barbecue",base,"Ventilatore a Pistola classico per Barbecue BBQ Classics","<a id=""maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""><p>Utilizza</a> i migliori barbecue alimentando il fuoco con il <strong>ventilatore a pistola classico per babecue BBQ Classics</strong>! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</p><p><a href=""http://www.bbqclassics.com"" target=""_blank""><strong>www.bbqclassics.com</strong></a></p><ul><li>Realizzato in plastica e metallo</li><li>Dimensioni: 25 x 18 x 4 cm circa</li></ul><p> Dimenzioni per Ventilatore a Pistola classico per Barbecue BBQ Classics: </br><ul><li>Altezza: 5.5 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 22 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158032706</p>","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",9.3,,,,Ventilatore-a-Pistola-classico-per-Barbecue-BBQ-Classics,"Ventilatore a Pistola classico per Barbecue BBQ Classics","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Barbecue,","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria",http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,,,,,"2016-09-13 09:56:07",,,,,,,,,,,,,,,,,"GTIN=8718158032706",60,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200353_92696.jpg,http://dropshipping.bigbuy.eu/imgs/V0200353_92694.jpg","GTIN=8718158032706",,,,,,,,,, +BB-V1600123,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""><p>Non</a> c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente. Con il <strong>contenitore portagiochi Frozen (32 x 23 cm)</strong> sarà semplicissimo!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni aprossimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> </p><p> Dimenzioni per Contenitore Portagiochi Frozen (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766006</p>","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Frozen-(32-x-23-cm),"Contenitore Portagiochi Frozen (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente",http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766006",48,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600123_93005.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93004.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93003.jpg","GTIN=8412842766006",,,,,,,,,, +BB-V1600124,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""><p>Desideri</a> sorprendere i più piccini con un regalo molto originale? Il <strong>contenitore portagiochi Spiderman (32 x 23 cm)</strong> decorerà e metterà in ordine le loro camerette.</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni approssimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766037</p>","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Spiderman--(32-x-23-cm),"Contenitore Portagiochi Spiderman (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette",http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766037",52,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600124_93008.jpg,http://dropshipping.bigbuy.eu/imgs/V1600124_93007.jpg","GTIN=8412842766037",,,,,,,,,, +BB-V1600125,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""><p>Insegna</a> ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del <strong>contenitore portagiochi Frozen (45 x 32 cm)</strong>. Il <strong>portagiocattoli</strong> che tutte le bambine sognano!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Frozen (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766129</p>","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Frozen-(45-x-32-cm),"Contenitore Portagiochi Frozen (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766129",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600125_93011.jpg,http://dropshipping.bigbuy.eu/imgs/V1600125_93009.jpg","GTIN=8412842766129",,,,,,,,,, +BB-V1600126,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""><p>I</a> piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> Spiderman</strong><strong> (45 x 32 cm)</strong>. Il <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> </strong>preferito dai bambini!</p><ul><li>Fabbricato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età raccomandata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766150</p>","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Spiderman-(45-x-32-cm),"Contenitore Portagiochi Spiderman (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766150",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600126_93014.jpg,http://dropshipping.bigbuy.eu/imgs/V1600126_93013.jpg","GTIN=8412842766150",,,,,,,,,, +BB-V1300154,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Spiderman (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Spiderman (4 pezzi)""><p>Vorresti</a> sorprendere i più piccoli della casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Spiderman (4 pezzi)</strong> li farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Spiderman (4 pezzi): </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 30.5 Cm</li><li>Peso: 0.283 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752232</p>","Vorresti sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Spiderman (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Spiderman (4 pezzi)""> Maggiori Informazioni</a>",0.283,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Spiderman-(4-pezzi),"Zaino per Piscina Spiderman (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Vorresti sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Spiderman (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300154_93570.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300154_93570.jpg,,,,,,"2016-08-16 13:42:17",,,,,,,,,,,,,,,,,"GTIN=7569000752232",129,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300154_93576.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93575.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93574.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93573.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93572.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93571.jpg","GTIN=7569000752232",,,,,,,,,, +BB-V1300156,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Frozen (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Frozen (4 pezzi)""><p>Vuoi</a> fare un <strong>regalo originale</strong> ai piccoli di casa? Se adorano il mare o la piscina, lo <strong>zaino per piscina Frozen (4 pezzi)</strong> li farà impazzire.</p><ul><li>Presenta una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: circa 24 x 31 x 6 cm</li></ul><p> Dimenzioni per Zaino per Piscina Frozen (4 pezzi): </br><ul><li>Altezza: 2 Cm</li><li>Larghezza: 23 Cm</li><li>Profondita': 30.5 Cm</li><li>Peso: 0.277 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752225</p>","Vuoi fare un regalo originale ai piccoli di casa? Se adorano il mare o la piscina, lo zaino per piscina Frozen (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Frozen (4 pezzi)""> Maggiori Informazioni</a>",0.277,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Frozen-(4-pezzi),"Zaino per Piscina Frozen (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Vuoi fare un regalo originale ai piccoli di casa? Se adorano il mare o la piscina, lo zaino per piscina Frozen (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300156_93580.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300156_93580.jpg,,,,,,"2016-08-26 13:31:11",,,,,,,,,,,,,,,,,"GTIN=7569000752225",104,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300156_93594.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93585.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93584.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93583.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93582.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93581.jpg","GTIN=7569000752225",,,,,,,,,, +BB-V1300157,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Minnie (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Minnie (4 pezzi)""><p>Ti</a> piacerebbe sorprendere le bimbe della casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Minnie (4 pezzi)</strong> le farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Minnie (4 pezzi): </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 24 Cm</li><li>Profondita': 30 Cm</li><li>Peso: 0.277 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934823796</p>","Ti piacerebbe sorprendere le bimbe della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minnie (4 pezzi) le farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Minnie (4 pezzi)""> Maggiori Informazioni</a>",0.277,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Minnie-(4-pezzi),"Zaino per Piscina Minnie (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Ti piacerebbe sorprendere le bimbe della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minnie (4 pezzi) le farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300157_93587.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300157_93587.jpg,,,,,,"2016-08-26 13:30:29",,,,,,,,,,,,,,,,,"GTIN=8427934823796",137,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300157_93593.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93592.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93591.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93590.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93589.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93586.jpg","GTIN=8427934823796",,,,,,,,,, +BB-V1300158,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Avengers (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Avengers (4 pezzi)""><p>Ti</a> piacerebbe sorprendere i più piccoli della casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Avengers (4 pezzi)</strong> li farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Avengers (4 pezzi): </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 23 Cm</li><li>Profondita': 31 Cm</li><li>Peso: 0.279 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752249</p>","Ti piacerebbe sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Avengers (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Avengers (4 pezzi)""> Maggiori Informazioni</a>",0.279,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Avengers-(4-pezzi),"Zaino per Piscina Avengers (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Ti piacerebbe sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Avengers (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300158_93596.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300158_93596.jpg,,,,,,"2016-08-26 11:29:45",,,,,,,,,,,,,,,,,"GTIN=7569000752249",139,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300158_93601.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93600.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93599.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93598.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93597.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93595.jpg","GTIN=7569000752249",,,,,,,,,, +BB-V1300159,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Minions (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Minions (4 pezzi)""><p>Ti</a> piacerebbe sorprendere i più piccoli di casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Minions (4 pezzi)</strong> li farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Minions (4 pezzi): </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 23 Cm</li><li>Profondita': 31 Cm</li><li>Peso: 0.279 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934823833</p>","Ti piacerebbe sorprendere i più piccoli di casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minions (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Minions (4 pezzi)""> Maggiori Informazioni</a>",0.279,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Minions-(4-pezzi),"Zaino per Piscina Minions (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Ti piacerebbe sorprendere i più piccoli di casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minions (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300159_93602.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300159_93602.jpg,,,,,,"2016-08-26 11:29:10",,,,,,,,,,,,,,,,,"GTIN=8427934823833",132,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300159_93608.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93607.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93606.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93605.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93604.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93603.jpg","GTIN=8427934823833",,,,,,,,,, +BB-G0500179,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Clip di Sicurezza a LED per Scarpe da Corsa GoFit","<a id=""maggiorni_informazioni"" title=""Clip di Sicurezza a LED per Scarpe da Corsa GoFit ""><p>Ti</a> piace allenarti la sera all'aria aperta? Allora, non dimenticare di indossare la tua <strong>clip di sicurezza a LED per scarpe da corsa GoFit</strong>! Grazie a questo utile <strong>accessorio da corsa</strong>, sarai visibile al buio mentre corri o ti alleni. Include 2 tipi di luce (fissa o intermittente) e può essere facilmente applicata sulla parte posteriore della scarpa o indossata intorno al polso. Possiede un bottone on/off che ti permette inoltre di cambiare il tipo di luce.  <strong>Progettato in Europa</strong> con materiali di alta qualità. 1 pezzo incluso.</p><p>Caratteristiche: </p><ul><li>LED verde per alta visibilità</li><li>Circa 100 ore di luce intermittente</li><li>Circa 70 ore di luce fissa</li><li>Adattabile a scrarpe da 6 a 8,5 cm di larghezza</li><li>Funziona a batterie (2 x CR2032, incluse)</li></ul><p> Dimenzioni per Clip di Sicurezza a LED per Scarpe da Corsa GoFit : </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 3.7 Cm</li><li>Peso: 0.095 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209116</p>","Ti piace allenarti la sera all'aria aperta? Allora, non dimenticare di indossare la tua clip di sicurezza a LED per scarpe da corsa GoFit! Grazie a questo utile accessorio da corsa, sarai visibile al buio mentre corri o ti alleni.</br><a href=""#maggiorni_informazioni"" title=""Clip di Sicurezza a LED per Scarpe da Corsa GoFit ""> Maggiori Informazioni</a>",0.095,1,"Taxable Goods","Catalog, Search",34.95,,,,Clip-di-Sicurezza-a-LED-per-Scarpe-da-Corsa-GoFit,"Clip di Sicurezza a LED per Scarpe da Corsa GoFit","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Ti piace allenarti la sera all'aria aperta? Allora, non dimenticare di indossare la tua clip di sicurezza a LED per scarpe da corsa GoFit! Grazie a questo utile accessorio da corsa, sarai visibile al buio mentre corri o ti alleni",http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit.jpg,,http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit.jpg,,,,,,"2016-08-22 08:23:08",,,,,,,,,,,,,,,,,"GTIN=8018417209116",207,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit_04.jpg,http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit_02.jpg,http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit_002.jpg","GTIN=8018417209116",,,,,,,,,, +BB-G0500187,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Sensore di Velocità Bluetooth GoFit","<a id=""maggiorni_informazioni"" title=""Sensore di Velocità Bluetooth GoFit""><p>Se</a> sei un amante del ciclismo e vuoi tener traccia della velocità, ritmo e distanza mentre pedali? Allora non perdere l'occasione, acquista il <strong>sensore di velocità Bluetooth GoFit</strong>, in modo da essere in grado di monitorare tutti i suoi dati in ogni momento, grazie al suo ingegnoso dispositivo! Devi solo installare il sensore e scaricare l'apposita applicazione sul tuo telefono cellulare.<br /><br />Questo <strong>sensore di velocità e ritmo </strong>è stato progettato in Europa ed è costituito di materiali resistenti all'acqua, in polimeri termoplastici. Molto semplice da installare. Compatibile con iOS (7.0 e successivi) e Android (4.3 e successivi). Funziona a batterie (1 x CR2032, incluse).</p><p>Include:</p><ul><li>1 sensore di velocità e ritmo (dimensioni: circa 8,5 x 7 x 1,5 cm)</li><li>1 magnete per ritmo pedale (dimensioni: circa 1,5 x 3,5 x 2 cm)</li><li>1 magnete per ruote</li><li>1 cacciavite</li><li>2 fascette</li><li>1 banda elastica</li></ul><p> Dimenzioni per Sensore di Velocità Bluetooth GoFit: </br><ul><li>Altezza: 20.3 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 3.5 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209109</p>","Se sei un amante del ciclismo e vuoi tener traccia della velocità, ritmo e distanza mentre pedali? Allora non perdere l'occasione, acquista il sensore di velocità Bluetooth GoFit, in modo da essere in grado di monitorare tutti i suoi dati in ogni momento, grazie al suo ingegnoso dispositivo! Devi solo installare il sensore e scaricare l'apposita applicazione sul tuo telefono cellulare.</br><a href=""#maggiorni_informazioni"" title=""Sensore di Velocità Bluetooth GoFit""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",179.9,,,,Sensore-di-Velocità-Bluetooth-GoFit,"Sensore di Velocità Bluetooth GoFit","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Se sei un amante del ciclismo e vuoi tener traccia della velocità, ritmo e distanza mentre pedali? Allora non perdere l'occasione, acquista il sensore di velocità Bluetooth GoFit, in modo da essere in grado di monitorare tutti i suoi dati in ogni mo",http://dropshipping.bigbuy.eu/imgs/G0500187_81130.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500187_81130.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417209109",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500187_81089.jpg,http://dropshipping.bigbuy.eu/imgs/G0500187_81088.jpg,http://dropshipping.bigbuy.eu/imgs/G0500187_81087.jpg,http://dropshipping.bigbuy.eu/imgs/G0500187_81086.jpg","GTIN=8018417209109",,,,,,,,,, +BB-G0500188,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)","<a id=""maggiorni_informazioni"" title=""Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)""><p>Da</a> ora in poi potrai fare jogging serenamente, sapendo che puoi essere visto dai veicoli intorno a te! <span id=""result_box"" lang=""en""><span class=""hps"">Basta inserire la <strong>luce a led di sicurezza </strong></span><strong>GoFit <span class=""hps atn"">(pacco da </span><span class=""hps"">2)</span> </strong>dentro ogni scarpa da corsa per aumentare la tua visibilità.<span class=""hps""> Le loro 2 potenti luci a LED verdi si attivano ad ogni passo che fai. È davvero semplice!</span><br /><br /><span class=""hps"">Ogni luce a LED funziona a pile</span> <span class=""hps atn"">(</span>2 x <span class=""hps"">CR1220</span>, 6 <span class=""hps"">V</span>, <span class=""hps"">incluse).</span> Durata delle batterie<span class=""hps"">: </span><span class=""hps"">150,000</span> <span class=""hps"">lampeggi di luce.</span> Queste luci a LED sono costituite di materiali di alta qualità e sono adatte all'utilizzo con i lacci con uno spessore massimo di 9 mm<span class=""hps"">.</span> <span class=""hps"">Include 2 unità. Dimensioni: circa</span> <span class=""hps"">4 x</span> <span class=""hps"">1,5</span> <span class=""hps"">x</span> <span class=""hps"">0,8</span> <span class=""hps"">cm.</span><br /></span></p><p> Dimenzioni per Luce a Led di Sicurezza per Lacci GoFit (pacco da 2): </br><ul><li>Altezza: 9 Cm</li><li>Larghezza: 3.7 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.061 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209482</p>","Da ora in poi potrai fare jogging serenamente, sapendo che puoi essere visto dai veicoli intorno a te! Basta inserire la luce a led di sicurezza GoFit (pacco da 2) dentro ogni scarpa da corsa per aumentare la tua visibilità.</br><a href=""#maggiorni_informazioni"" title=""Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)""> Maggiori Informazioni</a>",0.061,1,"Taxable Goods","Catalog, Search",33.95,,,,Luce-a-Led-di-Sicurezza-per-Lacci-GoFit-(pacco-da-2),"Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Da ora in poi potrai fare jogging serenamente, sapendo che puoi essere visto dai veicoli intorno a te! Basta inserire la luce a led di sicurezza GoFit (pacco da 2) dentro ogni scarpa da corsa per aumentare la tua visibilità",http://dropshipping.bigbuy.eu/imgs/G0500188_81129.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500188_81129.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417209482",238,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500188_81096.jpg,http://dropshipping.bigbuy.eu/imgs/G0500188_81095.jpg","GTIN=8018417209482",,,,,,,,,, +BB-G0500189,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Bracciale di Sicurezza LED GoFit","<a id=""maggiorni_informazioni"" title=""Bracciale di Sicurezza LED GoFit""><p>Non</a> possiedi ancora il <strong>bracciale di sicurezza LED</strong> <strong>GoFit</strong>? Non perdertelo e pratica i tuoi sport preferiti con questo leggero e comodo <strong>bracciale </strong>progettato in Europa. Qualunque auto o moto ti vedrà nel buio! Include 2 luci a LED e 2 modalità di illuminazione (fissa e lampeggiante) che puoi scegliere premendo il pulsante del bracciale. La cinghia di velcro lo fissa al braccio ed è flessibile e regolabile. La lunghezza massima è di circa 38,5 cm e la minima è di 31 cm. Funziona a batterie (2 x CR2023, incluse).</p><p> Dimenzioni per Bracciale di Sicurezza LED GoFit: </br><ul><li>Altezza: 9 Cm</li><li>Larghezza: 4 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.087 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209475</p>","Non possiedi ancora il bracciale di sicurezza LED GoFit? Non perdertelo e pratica i tuoi sport preferiti con questo leggero e comodo bracciale progettato in Europa.</br><a href=""#maggiorni_informazioni"" title=""Bracciale di Sicurezza LED GoFit""> Maggiori Informazioni</a>",0.087,1,"Taxable Goods","Catalog, Search",44.95,,,,Bracciale-di-Sicurezza-LED-GoFit,"Bracciale di Sicurezza LED GoFit","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Non possiedi ancora il bracciale di sicurezza LED GoFit? Non perdertelo e pratica i tuoi sport preferiti con questo leggero e comodo bracciale progettato in Europa",http://dropshipping.bigbuy.eu/imgs/G0500189_81128.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500189_81128.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417209475",93,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500189_81093.jpg,http://dropshipping.bigbuy.eu/imgs/G0500189_81092.jpg,http://dropshipping.bigbuy.eu/imgs/G0500189_81091.jpg","GTIN=8018417209475",,,,,,,,,, +BB-F1510306,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug Big Tribu Leopardato","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""><p>Affronta</a> il freddo invernale con l'originale<strong> coperta con<strong> maniche </strong><strong>Snug Snug <strong>Big</strong> <strong>Tribu</strong></strong>! </strong>Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche Big</strong> <strong>Tribu</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com"">www.snugsnug.com</a></strong></p><p> </p><p> </p><p> Dimenzioni per Coperta con Maniche Snug Snug Big Tribu: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 8.5 Cm</li><li>Profondita': 26.5 Cm</li><li>Peso: 0.602 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888103530</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""> Maggiori Informazioni</a>",0.602,1,"Taxable Goods","Catalog, Search",39.9,,,,Coperta-con-Maniche-Snug-Snug-Big-Tribu-Leopardato,"Coperta con Maniche Snug Snug Big Tribu Leopardato","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Leopardato,Leopardato,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle",http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,,,,,,"2015-12-30 17:30:06",,,,,,,,,,,,,,,,,"GTIN=4899888103530",46,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81363.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81362.jpg","GTIN=4899888103530",,,,,,,,,, +BB-F1510307,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug Big Tribu Zebra","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""><p>Affronta</a> il freddo invernale con l'originale<strong> coperta con<strong> maniche </strong><strong>Snug Snug <strong>Big</strong> <strong>Tribu</strong></strong>! </strong>Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche Big</strong> <strong>Tribu</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com"">www.snugsnug.com</a></strong></p><p> </p><p> </p><p> Dimenzioni per Coperta con Maniche Snug Snug Big Tribu: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 8.5 Cm</li><li>Profondita': 26.5 Cm</li><li>Peso: 0.602 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888103530</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""> Maggiori Informazioni</a>",0.602,1,"Taxable Goods","Catalog, Search",39.9,,,,Coperta-con-Maniche-Snug-Snug-Big-Tribu-Zebra,"Coperta con Maniche Snug Snug Big Tribu Zebra","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Zebra,Zebra,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle",http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,,,,,,"2016-01-21 08:26:10",,,,,,,,,,,,,,,,,"GTIN=4899888103530",486,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81363.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81362.jpg","GTIN=4899888103530",,,,,,,,,, +BB-F1510308,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug Big Tribu Dalmata","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""><p>Affronta</a> il freddo invernale con l'originale<strong> coperta con<strong> maniche </strong><strong>Snug Snug <strong>Big</strong> <strong>Tribu</strong></strong>! </strong>Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche Big</strong> <strong>Tribu</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com"">www.snugsnug.com</a></strong></p><p> </p><p> </p><p> Dimenzioni per Coperta con Maniche Snug Snug Big Tribu: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 8.5 Cm</li><li>Profondita': 26.5 Cm</li><li>Peso: 0.602 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888103530</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""> Maggiori Informazioni</a>",0.602,1,"Taxable Goods","Catalog, Search",39.9,,,,Coperta-con-Maniche-Snug-Snug-Big-Tribu-Dalmata,"Coperta con Maniche Snug Snug Big Tribu Dalmata","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Dalmata,Dalmata,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle",http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,,,,,,"2016-02-22 08:16:26",,,,,,,,,,,,,,,,,"GTIN=4899888103530",307,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81363.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81362.jpg","GTIN=4899888103530",,,,,,,,,, +BB-F1510302,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Azzurro","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Azzurro,"Coperta con Maniche Snug Snug One Big Azzurro","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Azzurro,Azzurro,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,,,,,,"2015-12-28 17:26:13",,,,,,,,,,,,,,,,,"GTIN=4899888102977",538,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-F1510303,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Rosso","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Rosso,"Coperta con Maniche Snug Snug One Big Rosso","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Rosso,Rosso,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,,,,,,"2016-01-20 10:35:30",,,,,,,,,,,,,,,,,"GTIN=4899888102977",600,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-F1510304,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Verde","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Verde,"Coperta con Maniche Snug Snug One Big Verde","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Verde,Verde,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,,,,,,"2015-10-06 11:20:02",,,,,,,,,,,,,,,,,"GTIN=4899888102977",764,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-F1510305,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Rosa","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Rosa,"Coperta con Maniche Snug Snug One Big Rosa","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Rosa,Rosa,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,,,,,,"2015-12-29 06:48:13",,,,,,,,,,,,,,,,,"GTIN=4899888102977",968,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-V1300145,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Accessori,Default Category/Relax Tempo Libero/Accessori/Ombrelli",base,"Ombrello Star Wars con LED","<a id=""maggiorni_informazioni"" title=""Ombrello Star Wars con LED""><p>I</a> fan di Guerre Stellari impazziranno con l'<strong>ombrello Star Wars con LED</strong>!</p><ul><li>Interruttore on/off sul manico</li><li>LED di vari colori sul manico centrale</li><li>Funziona a batterie (3 x AA, incluse)</li><li>Struttura: metallo, plastica e fibra di vetro</li><li>Cupola: poliestere (pongee)</li><li>Lunghezza approssimativa: 79,5 cm</li><li>Diametro approssimativo: 95 cm</li></ul><p> Dimenzioni per Ombrello Star Wars con LED: </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 79.5 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.458 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752317</p>","I fan di Guerre Stellari impazziranno con l'ombrello Star Wars con LED!Interruttore on/off sul manicoLED di vari colori sul manico centraleFunziona a batterie (3 x AA, incluse)Struttura: metallo, plastica e fibra di vetroCupola: poliestere (pongee)Lunghezza approssimativa: 79,5 cmDiametro approssimativo: 95 cm.</br><a href=""#maggiorni_informazioni"" title=""Ombrello Star Wars con LED""> Maggiori Informazioni</a>",0.458,1,"Taxable Goods","Catalog, Search",53.9,,,,Ombrello-Star-Wars-con-LED,"Ombrello Star Wars con LED","Moda Accessori,Moda,Accessori,Accessori,Ombrelli,","I fan di Guerre Stellari impazziranno con l'ombrello Star Wars con LED!Interruttore on/off sul manicoLED di vari colori sul manico centraleFunziona a batterie (3 x AA, incluse)Struttura: metallo, plastica e fibra di vetroCupola: poliestere (pongee)L",http://dropshipping.bigbuy.eu/imgs/V1300145_93662.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300145_93662.jpg,,,,,,"2016-09-12 07:48:53",,,,,,,,,,,,,,,,,"GTIN=7569000752317",23,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300145_93665.jpg,http://dropshipping.bigbuy.eu/imgs/V1300145_93664.jpg,http://dropshipping.bigbuy.eu/imgs/V1300145_93663.jpg,http://dropshipping.bigbuy.eu/imgs/V1300145_93661.jpg","GTIN=7569000752317",,,,,,,,,, +BB-H4530316,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Giocattoli e Giochi,Default Category/Giochi Bambini/Giocattoli e Giochi/Giochi educativi",base,"Elastici per fare bracciali con Perline di Frozen","<a id=""maggiorni_informazioni"" title=""Elastici per fare bracciali con Perline di Frozen""><p>Se</a> cerchi un <strong>gioco che intrattenga </strong>i tuoi figli e che sia l'ideale per essere alla moda, non perdere gli <strong>elastici per fare bracciali con le perline di Frozen</strong>. Contiene 130 elastici di diversi colori, perline di differenti forme con i protagonisti di Frozen, 1 gancino metallico, una chiusura a S, perline di diversi colori, 1 strumento per tenere gli elastici. Età consigliata: +5 anni. </p><p> </p><p> Dimenzioni per Elastici per fare bracciali con Perline di Frozen: </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 3.5 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8714274680036</p>","Se cerchi un gioco che intrattenga i tuoi figli e che sia l'ideale per essere alla moda, non perdere gli elastici per fare bracciali con le perline di Frozen.</br><a href=""#maggiorni_informazioni"" title=""Elastici per fare bracciali con Perline di Frozen""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",35,,,,Elastici-per-fare-bracciali-con-Perline-di-Frozen,"Elastici per fare bracciali con Perline di Frozen","Giochi Bambini,Giochi,Bambini,Giocattoli e Giochi,Giocattoli,Giochi,Giochi educativi,Giochi,educativi,","Se cerchi un gioco che intrattenga i tuoi figli e che sia l'ideale per essere alla moda, non perdere gli elastici per fare bracciali con le perline di Frozen",http://dropshipping.bigbuy.eu/imgs/H4530316_93063.jpg,,http://dropshipping.bigbuy.eu/imgs/H4530316_93063.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8714274680036",78,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H4530316_93067.jpg,http://dropshipping.bigbuy.eu/imgs/H4530316_93066.jpg,http://dropshipping.bigbuy.eu/imgs/H4530316_93065.jpg,http://dropshipping.bigbuy.eu/imgs/H4530316_93064.jpg","GTIN=8714274680036",,,,,,,,,, +BB-V1300134,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Cars Rosso","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Cars""><p>Vuoi</a> sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il <strong>berretto per bambini Cars</strong>. Dimensioni 54-56 cm. Misure della visiera: 14 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere</p><p> Dimenzioni per Berretto per Bambini Cars: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 17 Cm</li><li>Profondita': 21 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934797318</p>","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Cars""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",7.9,,,,Berretto-per-Bambini-Cars-Rosso,"Berretto per Bambini Cars Rosso","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Rosso,Rosso,","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars",http://dropshipping.bigbuy.eu/imgs/V1300133_91571.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300133_91571.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934797318",87,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300133_91229.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91197.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91196.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91195.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91194.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91193.jpg","GTIN=8427934797318",,,,,,,,,, +BB-V1300135,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Cars Nero","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Cars""><p>Vuoi</a> sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il <strong>berretto per bambini Cars</strong>. Dimensioni 54-56 cm. Misure della visiera: 14 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere</p><p> Dimenzioni per Berretto per Bambini Cars: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 17 Cm</li><li>Profondita': 21 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934797301</p>","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Cars""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",7.9,,,,Berretto-per-Bambini-Cars-Nero,"Berretto per Bambini Cars Nero","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Nero,Nero,","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars",http://dropshipping.bigbuy.eu/imgs/V1300133_91229.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300133_91229.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934797301",123,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300133_91571.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91197.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91196.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91195.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91194.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91193.jpg","GTIN=8427934797301",,,,,,,,,, +BB-V1300138,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Cappello per Bambini Batman vs Superman Blu Marino","<a id=""maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""><p>A</a> quale bambino non piace vantarsi dei suoi <strong>accessori moda</strong>? Sorprendi i più piccoli con il <strong>cappello per bambini Batman vs Superman</strong><strong> </strong>e quest'estate fai sì che siano ben protetti dai raggi solari. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65 % cotone e 35 % poliestere.</p><p> Dimenzioni per Cappello per Bambini Batman vs Superman: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 19 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934824359</p>","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari.</br><a href=""#maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",12.5,,,,Cappello-per-Bambini-Batman-vs-Superman-Blu Marino,"Cappello per Bambini Batman vs Superman Blu Marino","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Blu Marino,Blu Marino,","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari",http://dropshipping.bigbuy.eu/imgs/V1300136_91230.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300136_91230.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934824359",98,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300136_91231.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91201.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91200.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91199.jpg","GTIN=8427934824359",,,,,,,,,, +BB-V1300137,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Cappello per Bambini Batman vs Superman Grigio","<a id=""maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""><p>A</a> quale bambino non piace vantarsi dei suoi <strong>accessori moda</strong>? Sorprendi i più piccoli con il <strong>cappello per bambini Batman vs Superman</strong><strong> </strong>e quest'estate fai sì che siano ben protetti dai raggi solari. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65 % cotone e 35 % poliestere.</p><p> Dimenzioni per Cappello per Bambini Batman vs Superman: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 19 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934824335</p>","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari.</br><a href=""#maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",12.5,,,,Cappello-per-Bambini-Batman-vs-Superman-Grigio,"Cappello per Bambini Batman vs Superman Grigio","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Grigio,Grigio,","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari",http://dropshipping.bigbuy.eu/imgs/V1300136_91231.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300136_91231.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934824335",102,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300136_91230.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91201.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91200.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91199.jpg","GTIN=8427934824335",,,,,,,,,, +BB-V1300125,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Avengers Rosso","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Avengers""><p>A</a> quale bambino non piace mostrare gli <strong>accessori di moda</strong>? Sorprendili con il<strong> </strong><strong>berretto per bambini Avengers</strong> e proteggili dai raggi del sole di questa estate. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere.</p><p> Dimenzioni per Berretto per Bambini Avengers: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 18 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934792252</p>","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Avengers""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",10.9,,,,Berretto-per-Bambini-Avengers-Rosso,"Berretto per Bambini Avengers Rosso","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Rosso,Rosso,","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate",http://dropshipping.bigbuy.eu/imgs/V1300124_91179.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300124_91179.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934792252",101,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300124_91180.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91178.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91177.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91176.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91175.jpg","GTIN=8427934792252",,,,,,,,,, +BB-V1300126,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Avengers Nero","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Avengers""><p>A</a> quale bambino non piace mostrare gli <strong>accessori di moda</strong>? Sorprendili con il<strong> </strong><strong>berretto per bambini Avengers</strong> e proteggili dai raggi del sole di questa estate. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere.</p><p> Dimenzioni per Berretto per Bambini Avengers: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 18 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934792269</p>","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Avengers""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",10.9,,,,Berretto-per-Bambini-Avengers-Nero,"Berretto per Bambini Avengers Nero","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Nero,Nero,","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate",http://dropshipping.bigbuy.eu/imgs/V1300124_91180.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300124_91180.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934792269",92,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300124_91179.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91178.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91177.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91176.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91175.jpg","GTIN=8427934792269",,,,,,,,,, +BB-V0500179,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Stoviglie per bambini",base,"Posate Bambini Disney (5 pezzi) Minnie","<a id=""maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""><p>Quando</a> i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il <strong>set per mangiare </strong>da grandi, come le <strong>posate per bambini Disney (5 pezzi). </strong>Include: 1 piatto fondo, 1 piatto piano, 1 cucchiaio, 1 forchetta e 1 tazza.</p><ul><li>Età raccomandata: +12 mesi</li><li>Realizzato in polipropilene (senza BPA)</li><li>Adatto a lavastoviglie e microonde</li><li>Dimensioni del piatto fondo (diametro x altura): 15,5 x 3 cm circa</li><li>Dimensioni del piatto piano (diametro x altura): 22 x 1,5 cm circa</li><li>Lunghezza del cucchiaio: 15,5 cm circa</li><li>Lunghezza della forchetta: 15,5 cm circa</li><li>Dimensioni della tazza: 11 x 8,5 x 8,5 cm circa</li><li>Capacità della tazza: circa 250 ml</li><li>Conforme alla normativa UNE-EN 14372 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione: servizio di posate e utensili)</li><li>Conforme alla normativa UNE-EN 14350 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione liquida)</li></ul><p> Dimenzioni per Posate Bambini Disney (5 pezzi): </br><ul><li>Altezza: 27.5 Cm</li><li>Larghezza: 9.7 Cm</li><li>Profondita': 9 Cm</li><li>Peso: 0.487 Kg</li></ul></p><p>Codice Prodotto (EAN): 3662332013348</p>","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi).</br><a href=""#maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""> Maggiori Informazioni</a>",0.487,1,"Taxable Goods","Catalog, Search",41.5,,,,Posate-Bambini-Disney-(5-pezzi)-Minnie,"Posate Bambini Disney (5 pezzi) Minnie","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Stoviglie per bambini,Stoviglie,bambini,Modello Minnie,Minnie,","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi)",http://dropshipping.bigbuy.eu/imgs/V0500178_92050.jpg,,http://dropshipping.bigbuy.eu/imgs/V0500178_92050.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3662332013348",34,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0500178_92051.jpg,http://dropshipping.bigbuy.eu/imgs/V0500178_92049.jpg","GTIN=3662332013348",,,,,,,,,, +BB-V0500180,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Stoviglie per bambini",base,"Posate Bambini Disney (5 pezzi) Mickey","<a id=""maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""><p>Quando</a> i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il <strong>set per mangiare </strong>da grandi, come le <strong>posate per bambini Disney (5 pezzi). </strong>Include: 1 piatto fondo, 1 piatto piano, 1 cucchiaio, 1 forchetta e 1 tazza.</p><ul><li>Età raccomandata: +12 mesi</li><li>Realizzato in polipropilene (senza BPA)</li><li>Adatto a lavastoviglie e microonde</li><li>Dimensioni del piatto fondo (diametro x altura): 15,5 x 3 cm circa</li><li>Dimensioni del piatto piano (diametro x altura): 22 x 1,5 cm circa</li><li>Lunghezza del cucchiaio: 15,5 cm circa</li><li>Lunghezza della forchetta: 15,5 cm circa</li><li>Dimensioni della tazza: 11 x 8,5 x 8,5 cm circa</li><li>Capacità della tazza: circa 250 ml</li><li>Conforme alla normativa UNE-EN 14372 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione: servizio di posate e utensili)</li><li>Conforme alla normativa UNE-EN 14350 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione liquida)</li></ul><p> Dimenzioni per Posate Bambini Disney (5 pezzi): </br><ul><li>Altezza: 27.5 Cm</li><li>Larghezza: 9.7 Cm</li><li>Profondita': 9 Cm</li><li>Peso: 0.487 Kg</li></ul></p><p>Codice Prodotto (EAN): 3662332013355</p>","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi).</br><a href=""#maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""> Maggiori Informazioni</a>",0.487,1,"Taxable Goods","Catalog, Search",41.5,,,,Posate-Bambini-Disney-(5-pezzi)-Mickey,"Posate Bambini Disney (5 pezzi) Mickey","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Stoviglie per bambini,Stoviglie,bambini,Modello Mickey,Mickey,","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi)",http://dropshipping.bigbuy.eu/imgs/V0500178_92051.jpg,,http://dropshipping.bigbuy.eu/imgs/V0500178_92051.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3662332013355",37,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0500178_92050.jpg,http://dropshipping.bigbuy.eu/imgs/V0500178_92049.jpg","GTIN=3662332013355",,,,,,,,,, +BB-V1300179,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Ombrello per Bambini Pieghevole Star Wars","<a id=""maggiorni_informazioni"" title=""Ombrello per Bambini Pieghevole Star Wars""><p>Ti</a> presentiamo l'ombrello più galattico del pianeta, l'<strong><strong>ombrello per bambini pieghevole Star Wars</strong></strong>! Perfetto come <strong>regalo per bambini.</strong></p><ul><li>Struttura: 75 % metallo, 25 % plastica</li><li>Cupola: 100 % poliestere</li><li>Lunghezza: circa 23-52 cm</li><li>Diametro: circa 85 cm</li><li>Custodia inclusa</li></ul><p> </p><p> Dimenzioni per Ombrello per Bambini Pieghevole Star Wars: </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 6.5 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.255 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732739</p>","Ti presentiamo l'ombrello più galattico del pianeta, l'ombrello per bambini pieghevole Star Wars! Perfetto come regalo per bambini.</br><a href=""#maggiorni_informazioni"" title=""Ombrello per Bambini Pieghevole Star Wars""> Maggiori Informazioni</a>",0.255,1,"Taxable Goods","Catalog, Search",24.5,,,,Ombrello-per-Bambini-Pieghevole-Star-Wars,"Ombrello per Bambini Pieghevole Star Wars","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,","Ti presentiamo l'ombrello più galattico del pianeta, l'ombrello per bambini pieghevole Star Wars! Perfetto come regalo per bambini",http://dropshipping.bigbuy.eu/imgs/V1300179_101280.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300179_101280.jpg,,,,,,"2016-08-29 12:58:30",,,,,,,,,,,,,,,,,"GTIN=7569000732739",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300179_101281.jpg,http://dropshipping.bigbuy.eu/imgs/V1300179_101279.jpg,http://dropshipping.bigbuy.eu/imgs/V1300179_101278.jpg,http://dropshipping.bigbuy.eu/imgs/V1300179_101277.jpg","GTIN=7569000732739",,,,,,,,,, +BB-V1300195,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Passeggiate e viaggi",base,"Borsa Termica Porta Merenda Rubble (PAW Patrol)","<a id=""maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Rubble (PAW Patrol)""><p>Ti</a> presentiamo la <strong><strong>borsa termica porta merende Rubble (PAW Patrol)</strong></strong>! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla. Ideale per portare con sé il pranzo e la merenda.</p><ul><li>Dimensioni: circa 23 x 19 x 8 cm</li><li>Composizione: poliestere, schiuma di poliuretano e PEVA (polietilene di acetato di vinilo)</li></ul><p> Dimenzioni per Borsa Termica Porta Merenda Rubble (PAW Patrol): </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 22 Cm</li><li>Profondita': 26 Cm</li><li>Peso: 0.196 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732890</p>","Ti presentiamo la borsa termica porta merende Rubble (PAW Patrol)! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla.</br><a href=""#maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Rubble (PAW Patrol)""> Maggiori Informazioni</a>",0.196,1,"Taxable Goods","Catalog, Search",26.9,,,,Borsa-Termica-Porta-Merenda-Rubble-(PAW-Patrol),"Borsa Termica Porta Merenda Rubble (PAW Patrol)","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Passeggiate e viaggi,Passeggiate,viaggi,","Ti presentiamo la borsa termica porta merende Rubble (PAW Patrol)! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla",http://dropshipping.bigbuy.eu/imgs/V1300195_101259.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300195_101259.jpg,,,,,,"2016-08-26 13:36:55",,,,,,,,,,,,,,,,,"GTIN=7569000732890",59,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300195_101258.jpg,http://dropshipping.bigbuy.eu/imgs/V1300195_101257.jpg,http://dropshipping.bigbuy.eu/imgs/V1300195_101256.jpg","GTIN=7569000732890",,,,,,,,,, +BB-V1300196,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Passeggiate e viaggi",base,"Borsa Termica Porta Merenda Everest (PAW Patrol)","<a id=""maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Everest (PAW Patrol)""><p>Scopri</a> la<strong> <strong>borsa termica porta merenda Everest (PAW Patrol)</strong></strong> che sta facendo furore tra i bambini! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla. Perfetta per portare con sé il pranzo e la merenda.</p><ul><li>Dimensioni: circa 23 x 19 x 8 cm</li><li>Composizione: poliestere, schiuma di poliuretano e PEVA (polietilene di acetato di vinilo)</li></ul><p> Dimenzioni per Borsa Termica Porta Merenda Everest (PAW Patrol): </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 22 Cm</li><li>Profondita': 26 Cm</li><li>Peso: 0.196 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732906</p>","Scopri la borsa termica porta merenda Everest (PAW Patrol) che sta facendo furore tra i bambini! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla.</br><a href=""#maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Everest (PAW Patrol)""> Maggiori Informazioni</a>",0.196,1,"Taxable Goods","Catalog, Search",26.9,,,,Borsa-Termica-Porta-Merenda-Everest-(PAW-Patrol),"Borsa Termica Porta Merenda Everest (PAW Patrol)","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Passeggiate e viaggi,Passeggiate,viaggi,","Scopri la borsa termica porta merenda Everest (PAW Patrol) che sta facendo furore tra i bambini! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla",http://dropshipping.bigbuy.eu/imgs/V1300196_101260.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300196_101260.jpg,,,,,,"2016-08-26 13:36:36",,,,,,,,,,,,,,,,,"GTIN=7569000732906",51,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300196_101263.jpg,http://dropshipping.bigbuy.eu/imgs/V1300196_101262.jpg,http://dropshipping.bigbuy.eu/imgs/V1300196_101261.jpg","GTIN=7569000732906",,,,,,,,,, +BB-V1300198,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Passeggiate e viaggi",base,"Borsa Termica Porta Merenda Frozen","<a id=""maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Frozen""><p>Tutte</a> le bambine vogliono subito la <strong><strong><strong>borsa termica porta merenda</strong> Frozen</strong></strong>! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla. Perfetta per portare con sé il pranzo e la merenda.</p><ul><li>Dimensioni: circa 23 x 19 x 8 cm</li><li>Composizione: poliestere, schiuma di poliuretano e PEVA (polietilene di acetato di vinilo)</li></ul><p> Dimenzioni per Borsa Termica Porta Merenda Frozen: </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 22 Cm</li><li>Profondita': 26 Cm</li><li>Peso: 0.196 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732920</p>","Tutte le bambine vogliono subito la borsa termica porta merenda Frozen! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla.</br><a href=""#maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Frozen""> Maggiori Informazioni</a>",0.196,1,"Taxable Goods","Catalog, Search",26.9,,,,Borsa-Termica-Porta-Merenda-Frozen,"Borsa Termica Porta Merenda Frozen","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Passeggiate e viaggi,Passeggiate,viaggi,","Tutte le bambine vogliono subito la borsa termica porta merenda Frozen! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla",http://dropshipping.bigbuy.eu/imgs/V1300198_101268.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300198_101268.jpg,,,,,,"2016-08-30 07:32:17",,,,,,,,,,,,,,,,,"GTIN=7569000732920",52,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300198_101271.jpg,http://dropshipping.bigbuy.eu/imgs/V1300198_101270.jpg,http://dropshipping.bigbuy.eu/imgs/V1300198_101269.jpg","GTIN=7569000732920",,,,,,,,,, +BB-V1300204,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Materiale Scolastico,Default Category/Giochi Bambini/Materiale Scolastico/Astucci e portapenne",base,"Astuccio Scuola 3D Frozen","<a id=""maggiorni_informazioni"" title=""Astuccio Scuola 3D Frozen""><p>Le</a> piccola fan delle principesse Anna ed Elsa non possono tornare a scuola senza l'<strong>astuccio scuola</strong> <strong>3D Frozen</strong>!</p><ul><li>Cinque scompartimenti separati</li><li>Due cerniere</li><li>Dimensioni: 21,5 x 12 x 10 cm circa</li><li>Composizione: poliestere</li></ul><p> Dimenzioni per Astuccio Scuola 3D Frozen: </br><ul><li>Altezza: 2 Cm</li><li>Larghezza: 24 Cm</li><li>Profondita': 14 Cm</li><li>Peso: 0.099 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000733248</p>","Le piccola fan delle principesse Anna ed Elsa non possono tornare a scuola senza l'astuccio scuola 3D Frozen!Cinque scompartimenti separatiDue cerniereDimensioni: 21,5 x 12 x 10 cm circaComposizione: poliestere.</br><a href=""#maggiorni_informazioni"" title=""Astuccio Scuola 3D Frozen""> Maggiori Informazioni</a>",0.099,1,"Taxable Goods","Catalog, Search",19.9,,,,Astuccio-Scuola-3D-Frozen,"Astuccio Scuola 3D Frozen","Giochi Bambini,Giochi,Bambini,Materiale Scolastico,Materiale,Scolastico,Astucci e portapenne,Astucci,portapenne,","Le piccola fan delle principesse Anna ed Elsa non possono tornare a scuola senza l'astuccio scuola 3D Frozen!Cinque scompartimenti separatiDue cerniereDimensioni: 21,5 x 12 x 10 cm circaComposizione: poliestere",http://dropshipping.bigbuy.eu/imgs/V1300204_102661.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300204_102661.jpg,,,,,,"2016-09-14 07:51:11",,,,,,,,,,,,,,,,,"GTIN=7569000733248",138,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300204_102663.jpg,http://dropshipping.bigbuy.eu/imgs/V1300204_102662.jpg","GTIN=7569000733248",,,,,,,,,, +BB-V1300208,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Materiale Scolastico,Default Category/Giochi Bambini/Materiale Scolastico/Zaini scuola",base,"Zaino-Sacca Frozen","<a id=""maggiorni_informazioni"" title=""Zaino-Sacca Frozen""><p>Lo</a> <strong>zaino-sacca Frozen </strong>è lo zaino che sta facendo furore tra le bambine!</p><ul><li>Realizzato in poliestere</li><li>Manico superiore e cinghie con velcro</li><li>Tasca frontale con cerniera</li><li>Dimensioni: circa 33 x 44 cm</li></ul><p> </p><p> Dimenzioni per Zaino-Sacca Frozen: </br><ul><li>Altezza: 0.5 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 46 Cm</li><li>Peso: 0.138 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000733286</p>","Lo zaino-sacca Frozen è lo zaino che sta facendo furore tra le bambine!Realizzato in poliestereManico superiore e cinghie con velcroTasca frontale con cernieraDimensioni: circa 33 x 44 cm .</br><a href=""#maggiorni_informazioni"" title=""Zaino-Sacca Frozen""> Maggiori Informazioni</a>",0.138,1,"Taxable Goods","Catalog, Search",24.9,,,,Zaino-Sacca-Frozen,"Zaino-Sacca Frozen","Giochi Bambini,Giochi,Bambini,Materiale Scolastico,Materiale,Scolastico,Zaini scuola,Zaini,scuola,","Lo zaino-sacca Frozen è lo zaino che sta facendo furore tra le bambine!Realizzato in poliestereManico superiore e cinghie con velcroTasca frontale con cernieraDimensioni: circa 33 x 44 cm ",http://dropshipping.bigbuy.eu/imgs/V1300208_102667.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300208_102667.jpg,,,,,,"2016-09-14 07:50:58",,,,,,,,,,,,,,,,,"GTIN=7569000733286",141,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300208_102669.jpg,http://dropshipping.bigbuy.eu/imgs/V1300208_102668.jpg","GTIN=7569000733286",,,,,,,,,, +BB-I4115041,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cover e custodie",base,"Custodia Impermeabile per Cellulare WpShield Azzurro","<a id=""maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""><p>Sei</a> una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la <strong>custodia impermeabile per cellulare WpShield</strong>. Con questa custodia impermeabile potrai portare il telefono ovunque ti piaccia, anche per un tuffo in piscina o sul mare.<br /><br /><a href=""http://www.waterproofshield.com/""><strong>www.waterproofshield.com</strong></a><br /><br />Questa custodia impermeabile dispone sia di un cinturino con chiusura a velcro (lunghezza massima: circa 37 cm) e un cavo con chiusura di sicurezza da indossare al collo (lunghezza massima: circa 60 cm). Composizione: PVC (Spessore: circa 3 mm). Dimensioni: circa 10,5 x 15,5 cm.</p><p> Dimenzioni per Custodia Impermeabile per Cellulare WpShield : </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 11.1 Cm</li><li>Profondita': 1.9 Cm</li><li>Peso: 0.06 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106722</p>","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield.</br><a href=""#maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""> Maggiori Informazioni</a>",0.06,1,"Taxable Goods","Catalog, Search",14.5,,,,Custodia-Impermeabile-per-Cellulare-WpShield-Azzurro,"Custodia Impermeabile per Cellulare WpShield Azzurro","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cover e custodie,Cover,custodie,Colore Azzurro,Azzurro,","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield",http://dropshipping.bigbuy.eu/imgs/I4115040_78768.jpg,,http://dropshipping.bigbuy.eu/imgs/I4115040_78768.jpg,,,,,,"2015-12-21 12:09:49",,,,,,,,,,,,,,,,,"GTIN=4899888106722",4125,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4115040_78770.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78769.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78767.jpg","GTIN=4899888106722",,,,,,,,,, +BB-I4115042,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cover e custodie",base,"Custodia Impermeabile per Cellulare WpShield Bianco","<a id=""maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""><p>Sei</a> una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la <strong>custodia impermeabile per cellulare WpShield</strong>. Con questa custodia impermeabile potrai portare il telefono ovunque ti piaccia, anche per un tuffo in piscina o sul mare.<br /><br /><a href=""http://www.waterproofshield.com/""><strong>www.waterproofshield.com</strong></a><br /><br />Questa custodia impermeabile dispone sia di un cinturino con chiusura a velcro (lunghezza massima: circa 37 cm) e un cavo con chiusura di sicurezza da indossare al collo (lunghezza massima: circa 60 cm). Composizione: PVC (Spessore: circa 3 mm). Dimensioni: circa 10,5 x 15,5 cm.</p><p> Dimenzioni per Custodia Impermeabile per Cellulare WpShield : </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 11.1 Cm</li><li>Profondita': 1.9 Cm</li><li>Peso: 0.06 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106739</p>","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield.</br><a href=""#maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""> Maggiori Informazioni</a>",0.06,1,"Taxable Goods","Catalog, Search",14.5,,,,Custodia-Impermeabile-per-Cellulare-WpShield-Bianco,"Custodia Impermeabile per Cellulare WpShield Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cover e custodie,Cover,custodie,Colore Bianco,Bianco,","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield",http://dropshipping.bigbuy.eu/imgs/I4115040_78770.jpg,,http://dropshipping.bigbuy.eu/imgs/I4115040_78770.jpg,,,,,,"2015-12-21 12:10:05",,,,,,,,,,,,,,,,,"GTIN=4899888106739",4321,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4115040_78768.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78769.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78767.jpg","GTIN=4899888106739",,,,,,,,,, +BB-I4115044,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Batterie, Caricatori, Adattatori",base,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Bianco","<a id=""maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""><p>Non</a> uscire di casa senza la <strong>doppia porta USB con presa elettrica e caricabatteria da auto Pocken</strong> per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina. Questo caricatore con doppio ingresso USB è molto pratico e semplice da usare. Dimensioni approssimative: 6 x 6 x 3,5 cm.</p><p><strong><a href=""http://www.pockenrg.com"">www.pockenrg.com</a>  </strong></p><div><div>Caratteristiche tecniche:</div><ul><li>Ingresso AC: 100-240 V / 0.15 A / 50-60 Hz</li><li>Ingresso DC: 12-24 V / 0.35 A</li><li>Uscita DC: +5  V / 1 A</li></ul></div><p> Dimenzioni per Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken : </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 6.5 Cm</li><li>Profondita': 6.5 Cm</li><li>Peso: 0.077 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106944</p>","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina.</br><a href=""#maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""> Maggiori Informazioni</a>",0.077,1,"Taxable Goods","Catalog, Search",29.99,,,,Doppia-Porta-USB-con-Presa-Elettrica-e-Caricabatteria-da-Auto-Pocken-Bianco,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Bianco","Informatica Elettronica,Informatica,Elettronica,Batterie, Caricatori, Adattatori,Batterie,,Caricatori,,Adattatori,Colore Bianco,Bianco,","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina",http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0002.jpg,,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0002.jpg,,,,,,"2015-05-14 07:58:09",,,,,,,,,,,,,,,,,"GTIN=4899888106944",161,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_00.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_08.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_04.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_03.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0003.jpg","GTIN=4899888106944",,,,,,,,,, +BB-I4115045,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Batterie, Caricatori, Adattatori",base,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Nero","<a id=""maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""><p>Non</a> uscire di casa senza la <strong>doppia porta USB con presa elettrica e caricabatteria da auto Pocken</strong> per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina. Questo caricatore con doppio ingresso USB è molto pratico e semplice da usare. Dimensioni approssimative: 6 x 6 x 3,5 cm.</p><p><strong><a href=""http://www.pockenrg.com"">www.pockenrg.com</a>  </strong></p><div><div>Caratteristiche tecniche:</div><ul><li>Ingresso AC: 100-240 V / 0.15 A / 50-60 Hz</li><li>Ingresso DC: 12-24 V / 0.35 A</li><li>Uscita DC: +5  V / 1 A</li></ul></div><p> Dimenzioni per Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken : </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 6.5 Cm</li><li>Profondita': 6.5 Cm</li><li>Peso: 0.077 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106678</p>","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina.</br><a href=""#maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""> Maggiori Informazioni</a>",0.077,1,"Taxable Goods","Catalog, Search",29.99,,,,Doppia-Porta-USB-con-Presa-Elettrica-e-Caricabatteria-da-Auto-Pocken-Nero,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Nero","Informatica Elettronica,Informatica,Elettronica,Batterie, Caricatori, Adattatori,Batterie,,Caricatori,,Adattatori,Colore Nero,Nero,","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina",http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_00.jpg,,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_00.jpg,,,,,,"2015-08-18 12:37:09",,,,,,,,,,,,,,,,,"GTIN=4899888106678",265,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0002.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_08.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_04.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_03.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0003.jpg","GTIN=4899888106678",,,,,,,,,, +BB-I3505259,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Auricolari da Corsa GoFit Bianco","<a id=""maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""><p>Fai</a> sport mentre ascolti la musica o parli al telefono indossando questi<strong> auricolari da corsa</strong>! Questi <strong>auricolari sportivi GoFit</strong> sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza. Progettate specificamente per gli altleti. Con materiali speciali e ottima qualità sonora. Caratteristiche:</p><ul><li>Suono: stereo</li><li>Connessione audio: cavo con uscita 3,5 mm</li><li>Microfono integrato</li><li>Pulsante di risposta alla chiamata</li><li>Risposta in frequenza: 20-20000 Hz</li><li>Intensità del suono: 93 dB</li><li>Impedenza dello speaker: 32 Ω</li><li>Adatto agli iPhone, smartphone e telefoni cellulari</li></ul><p> Dimenzioni per Auricolari da Corsa GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 2.7 Cm</li><li>Peso: 0.079 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417204005</p>","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza.</br><a href=""#maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""> Maggiori Informazioni</a>",0.079,1,"Taxable Goods","Catalog, Search",38.9,,,,Auricolari-da-Corsa-GoFit-Bianco,"Auricolari da Corsa GoFit Bianco","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Bianco,Bianco,","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza",http://dropshipping.bigbuy.eu/imgs/I3505223_80641.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505223_80641.jpg,,,,,,"2015-09-23 10:52:34",,,,,,,,,,,,,,,,,"GTIN=8018417204005",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505223_80644.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80643.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80642.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80640.jpg","GTIN=8018417204005",,,,,,,,,, +BB-I3505260,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Auricolari da Corsa GoFit Arancio","<a id=""maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""><p>Fai</a> sport mentre ascolti la musica o parli al telefono indossando questi<strong> auricolari da corsa</strong>! Questi <strong>auricolari sportivi GoFit</strong> sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza. Progettate specificamente per gli altleti. Con materiali speciali e ottima qualità sonora. Caratteristiche:</p><ul><li>Suono: stereo</li><li>Connessione audio: cavo con uscita 3,5 mm</li><li>Microfono integrato</li><li>Pulsante di risposta alla chiamata</li><li>Risposta in frequenza: 20-20000 Hz</li><li>Intensità del suono: 93 dB</li><li>Impedenza dello speaker: 32 Ω</li><li>Adatto agli iPhone, smartphone e telefoni cellulari</li></ul><p> Dimenzioni per Auricolari da Corsa GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 2.7 Cm</li><li>Peso: 0.079 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209512</p>","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza.</br><a href=""#maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""> Maggiori Informazioni</a>",0.079,1,"Taxable Goods","Catalog, Search",38.9,,,,Auricolari-da-Corsa-GoFit-Arancio,"Auricolari da Corsa GoFit Arancio","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Arancio,Arancio,","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza",http://dropshipping.bigbuy.eu/imgs/I3505223_80644.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505223_80644.jpg,,,,,,"2015-09-23 10:52:34",,,,,,,,,,,,,,,,,"GTIN=8018417209512",43,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505223_80641.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80643.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80642.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80640.jpg","GTIN=8018417209512",,,,,,,,,, +BB-I3505248,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Bluetooth Portatile AudioSonic SK1511 Azzurro","<a id=""maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""><p>Se</a> adori la musica e la tecnologia, l'<strong>altoparlante Bluetooth portatile AudioSonic</strong> è l'ideale per te! Riesci ad immaginare di indossare questo <strong>altoparlante</strong> intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone?<strong> </strong>Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm. Dimensioni: circa 5,5 x 7 x 1 cm. Raggio del bluetooth: circa 10 m.</p><p>Caratteristiche:</p><ul><li>Batteria ricaricabile Li-ion</li><li>Vita della batteria: 4 ore</li><li>Batteria 300 mAh</li><li>Microfono incorporato</li><li>Controllo a mani libere</li><li>Pulsanti di controllo</li><li>Porta micro USB porta: 5 V</li><li>Porta input Aux</li><li>Potenza: 3 W</li></ul><p>Include:</p><ul><li>Cavo USB + micro USB</li><li>Cavo dual jack da 3,5 mm</li></ul><p> Dimenzioni per Altoparlante Bluetooth Portatile AudioSonic: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 4.6 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016015112</p>","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone? Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",33.35,,,,Altoparlante-Bluetooth-Portatile-AudioSonic-SK1511 Azzurro,"Altoparlante Bluetooth Portatile AudioSonic SK1511 Azzurro","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,Referenza e Colore SK1511 Azzurro,SK1511 Azzurro,","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smart",http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,,http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,,,,,,"2015-12-21 11:15:29",,,,,,,,,,,,,,,,,"GTIN=8713016015112",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_02.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_0004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_04.jpg","GTIN=8713016015112",,,,,,,,,, +BB-I3505249,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Bluetooth Portatile AudioSonic SK1513 Rosa","<a id=""maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""><p>Se</a> adori la musica e la tecnologia, l'<strong>altoparlante Bluetooth portatile AudioSonic</strong> è l'ideale per te! Riesci ad immaginare di indossare questo <strong>altoparlante</strong> intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone?<strong> </strong>Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm. Dimensioni: circa 5,5 x 7 x 1 cm. Raggio del bluetooth: circa 10 m.</p><p>Caratteristiche:</p><ul><li>Batteria ricaricabile Li-ion</li><li>Vita della batteria: 4 ore</li><li>Batteria 300 mAh</li><li>Microfono incorporato</li><li>Controllo a mani libere</li><li>Pulsanti di controllo</li><li>Porta micro USB porta: 5 V</li><li>Porta input Aux</li><li>Potenza: 3 W</li></ul><p>Include:</p><ul><li>Cavo USB + micro USB</li><li>Cavo dual jack da 3,5 mm</li></ul><p> Dimenzioni per Altoparlante Bluetooth Portatile AudioSonic: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 4.6 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016000996</p>","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone? Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",33.35,,,,Altoparlante-Bluetooth-Portatile-AudioSonic-SK1513 Rosa,"Altoparlante Bluetooth Portatile AudioSonic SK1513 Rosa","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,Referenza e Colore SK1513 Rosa,SK1513 Rosa,","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smart",http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,,,,,,"2015-06-01 09:01:55",,,,,,,,,,,,,,,,,"GTIN=8713016000996",16,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_02.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_0004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_04.jpg","GTIN=8713016000996",,,,,,,,,, +BB-I3505250,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Bluetooth Portatile AudioSonic SK1512 Verde","<a id=""maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""><p>Se</a> adori la musica e la tecnologia, l'<strong>altoparlante Bluetooth portatile AudioSonic</strong> è l'ideale per te! Riesci ad immaginare di indossare questo <strong>altoparlante</strong> intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone?<strong> </strong>Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm. Dimensioni: circa 5,5 x 7 x 1 cm. Raggio del bluetooth: circa 10 m.</p><p>Caratteristiche:</p><ul><li>Batteria ricaricabile Li-ion</li><li>Vita della batteria: 4 ore</li><li>Batteria 300 mAh</li><li>Microfono incorporato</li><li>Controllo a mani libere</li><li>Pulsanti di controllo</li><li>Porta micro USB porta: 5 V</li><li>Porta input Aux</li><li>Potenza: 3 W</li></ul><p>Include:</p><ul><li>Cavo USB + micro USB</li><li>Cavo dual jack da 3,5 mm</li></ul><p> Dimenzioni per Altoparlante Bluetooth Portatile AudioSonic: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 4.6 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016000972</p>","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone? Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",33.35,,,,Altoparlante-Bluetooth-Portatile-AudioSonic-SK1512 Verde,"Altoparlante Bluetooth Portatile AudioSonic SK1512 Verde","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,Referenza e Colore SK1512 Verde,SK1512 Verde,","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smart",http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,,http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,,,,,,"2016-02-01 10:38:01",,,,,,,,,,,,,,,,,"GTIN=8713016000972",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_02.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_0004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_04.jpg","GTIN=8713016000972",,,,,,,,,, +BB-I3505262,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Auricolari da Corsa GoFit","<a id=""maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""><p>Ora,</a> ascoltare la musica o fare una chiamata non saranno più scuse valide per non allenarsi, grazie agli <strong>auricolari da corsa GoFit</strong>! Sono davvero comodi e pratici <strong>auricolari </strong>che si adattano completamente all'orecchio, Speciale <strong>design Europeo</strong> per l'utilizzo in allenamento. Materiali e suono di alta qualità. Include una piccola custodia in tessuto per conservare gli auricolari.</p><p>Caratteristiche:</p><ul><li>Flessibili e resistenti all'acqua</li><li>Suono: stereo</li><li>Connessione audio: cavo jack 3,5 mm</li><li>Microfono incorporato</li><li>Pulsante di risposta e termine chiamata</li><li>Risposta di frequenza: 20-20000 Hz</li><li>Livello sonoro: 93 dB</li><li>Impedenza altoparlante: 32 Ω</li><li>Adatti all'uso con iPhone, smartphone e altri cellulari</li></ul><p> Dimenzioni per Auricolari da Corsa GoFit: </br><ul><li>Altezza: 20.5 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417208119</p>","Ora, ascoltare la musica o fare una chiamata non saranno più scuse valide per non allenarsi, grazie agli auricolari da corsa GoFit! Sono davvero comodi e pratici auricolari che si adattano completamente all'orecchio, Speciale design Europeo per l'utilizzo in allenamento.</br><a href=""#maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",49.9,,,,Auricolari-da-Corsa-GoFit,"Auricolari da Corsa GoFit","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,","Ora, ascoltare la musica o fare una chiamata non saranno più scuse valide per non allenarsi, grazie agli auricolari da corsa GoFit! Sono davvero comodi e pratici auricolari che si adattano completamente all'orecchio, Speciale design Europeo per l'ut",http://dropshipping.bigbuy.eu/imgs/I3505262_80647.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505262_80647.jpg,,,,,,"2016-09-07 15:19:36",,,,,,,,,,,,,,,,,"GTIN=8018417208119",46,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505262_80646.jpg,http://dropshipping.bigbuy.eu/imgs/I3505262_80645.jpg","GTIN=8018417208119",,,,,,,,,, +BB-G0500185,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Fascia Sportiva con Auricolari GoFit Verde","<a id=""maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""><p>Se</a> adori gli sport e ti piace tenerti informato con gli ultimi <strong>accessori sportivi</strong>, non puoi perderti questa ottima <strong>fascia sportiva con auricolari GoFit</strong>. Con questa fascia per la testa, potrai ascoltare i tuoi brani musicali preferiti mentre fai jogging, inoltre potrai rispondere alle chiamate, semplicemente premendo il pulsante di risposta e chiusura chiamate incorporato nella doppia connessione, con cavo audio jack da 3,5 mm (lunghezza: circa 1 m). Altoparlanti rimovibili per permetterti di lavare la fascia. Ampiezza: circa 9,5 cm. Diametro: circa 27 cm. Esterno 100% poliestere. Interno in microfibra polare.<br /><br />Caratteristiche:</p><ul><li>Sensibilità: 5 dB</li><li>Impedenza altoparlante: 32 Ω</li><li>Frequenza: 20 Hz-20 kHz</li><li>Adatto a smartphone e altri telefoni mobili</li></ul><p> Dimenzioni per Fascia Sportiva con Auricolari GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 4.5 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209635</p>","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit.</br><a href=""#maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",49.99,,,,Fascia-Sportiva-con-Auricolari-GoFit-Verde,"Fascia Sportiva con Auricolari GoFit Verde","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Verde,Verde,","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit",http://dropshipping.bigbuy.eu/imgs/G0500184_81112.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500184_81112.jpg,,,,,,"2015-09-28 07:07:26",,,,,,,,,,,,,,,,,"GTIN=8018417209635",11,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500184_81118.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81117.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81116.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81115.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81114.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81113.jpg","GTIN=8018417209635",,,,,,,,,, +BB-G0500186,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Fascia Sportiva con Auricolari GoFit Arancio","<a id=""maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""><p>Se</a> adori gli sport e ti piace tenerti informato con gli ultimi <strong>accessori sportivi</strong>, non puoi perderti questa ottima <strong>fascia sportiva con auricolari GoFit</strong>. Con questa fascia per la testa, potrai ascoltare i tuoi brani musicali preferiti mentre fai jogging, inoltre potrai rispondere alle chiamate, semplicemente premendo il pulsante di risposta e chiusura chiamate incorporato nella doppia connessione, con cavo audio jack da 3,5 mm (lunghezza: circa 1 m). Altoparlanti rimovibili per permetterti di lavare la fascia. Ampiezza: circa 9,5 cm. Diametro: circa 27 cm. Esterno 100% poliestere. Interno in microfibra polare.<br /><br />Caratteristiche:</p><ul><li>Sensibilità: 5 dB</li><li>Impedenza altoparlante: 32 Ω</li><li>Frequenza: 20 Hz-20 kHz</li><li>Adatto a smartphone e altri telefoni mobili</li></ul><p> Dimenzioni per Fascia Sportiva con Auricolari GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 4.5 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209505</p>","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit.</br><a href=""#maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",49.99,,,,Fascia-Sportiva-con-Auricolari-GoFit-Arancio,"Fascia Sportiva con Auricolari GoFit Arancio","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Arancio,Arancio,","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit",http://dropshipping.bigbuy.eu/imgs/G0500184_81118.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500184_81118.jpg,,,,,,"2015-09-28 07:07:51",,,,,,,,,,,,,,,,,"GTIN=8018417209505",32,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500184_81112.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81117.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81116.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81115.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81114.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81113.jpg","GTIN=8018417209505",,,,,,,,,, +BB-I3505265,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Sportivo Bluetooth GoFit","<a id=""maggiorni_informazioni"" title=""Altoparlante Sportivo Bluetooth GoFit""><p>Sei</a> un amante dello sport e adori fare escursioni in montagna? Ti piace la musica? Allora questo prodotto ti starà a pennello! Questo favoloso <strong>altoparlante sportivo Bluetooth GoFit</strong> ti accompagnerà dove vuoi, permettendoti di ascoltare la tua musica preferita durante le tue uscite, così come rispondere alle chiamate attraverso la funzione Bluetooth. Resistente all'acqua. Include un cavo di ricarica USB. L'altoparlante ha una clip per inserirlo su una cintura e una banda elastica per indossarlo al polso, nello zaino, ecc. Misure (diametro x altezza) circa: 8,5 cm x 3 cm. Peso: circa 159 g.</p><p>Caratteristiche:</p><ul><li>Protezione impermeabile: IP4</li><li>Bluetooth 2.0 + EDR: distanza fino a circa 10 m</li><li>Funzione mani libere</li><li>Permette la ricezione di chiamate e terminarle</li><li>Pulsanti di pausa, avanzamento e indietro</li><li>Tempo di riproduzione: circa 2,5 ore</li><li>Frequenza: 90 Hz-20 KHz</li><li>Uscita altoparlante: 5 W</li><li>Connessione per audio jack<em> </em>3,5 mm</li></ul><p> </p><p> Dimenzioni per Altoparlante Sportivo Bluetooth GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 6 Cm</li><li>Profondita': 9.5 Cm</li><li>Peso: 0.278 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417207747</p>","Sei un amante dello sport e adori fare escursioni in montagna? Ti piace la musica? Allora questo prodotto ti starà a pennello! Questo favoloso altoparlante sportivo Bluetooth GoFit ti accompagnerà dove vuoi, permettendoti di ascoltare la tua musica preferita durante le tue uscite, così come rispondere alle chiamate attraverso la funzione Bluetooth.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Sportivo Bluetooth GoFit""> Maggiori Informazioni</a>",0.278,1,"Taxable Goods","Catalog, Search",89.9,,,,Altoparlante-Sportivo-Bluetooth-GoFit,"Altoparlante Sportivo Bluetooth GoFit","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,","Sei un amante dello sport e adori fare escursioni in montagna? Ti piace la musica? Allora questo prodotto ti starà a pennello! Questo favoloso altoparlante sportivo Bluetooth GoFit ti accompagnerà dove vuoi, permettendoti di ascoltare la tua musica ",http://dropshipping.bigbuy.eu/imgs/I3505265_81327.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505265_81327.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417207747",10,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505265_81169.jpg,http://dropshipping.bigbuy.eu/imgs/I3505265_81168.jpg,http://dropshipping.bigbuy.eu/imgs/I3505265_81167.jpg","GTIN=8018417207747",,,,,,,,,, +BB-H1000173,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Scooter Elettrici",base,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Azzurro","<a id=""maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""><p>Dimentica</a> i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà. Metti alla prova il tuo equilibrio e divertiti!</p><p><br /><strong><a href=""http://www.roverdroid.com/"">www.roverdroid.com</a></strong></p><p>Caratteristiche:</p><ul><li>Batteria a litio: 160 Wh</li><li>Pulsante on/off</li><li>Indicatore luminoso del livello di carica sull'asse centrale</li><li>Illuminazione di segnaletica anteriore LED</li><li>Zona d'appoggio in gomma resistente e antiscivolo</li><li>Velocità massima: 10 km/h circa</li><li>Autonomia con carica completa: da 17 a 20 km circa</li><li>Peso: 10 kg circa</li><li>Tempo di carica: 3 ore circa</li><li>Peso massimo supportato: 100 kg</li><li>Dimensioni: 58 x 18 x 18 cm circa</li><li>Diametro delle ruote: 7""</li><li>Borsa di trasporto e caricabatterie inclusi (CA: 100-240 V, 50-60 Hz, 1,2 A / DC: 36 V, 2 A)</li></ul><p> Dimenzioni per Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid: </br><ul><li>Altezza: 22.5 Cm</li><li>Larghezza: 64 Cm</li><li>Profondita': 24 Cm</li><li>Peso: 11.85 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109204</p>","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà.</br><a href=""#maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""> Maggiori Informazioni</a>",11.85,1,"Taxable Goods","Catalog, Search",599,,,,Mini-Scooter-Elettrico-di-Auto-Equilibrio-(2-ruote)-Rover-Droid-Azzurro,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Azzurro","Informatica Elettronica,Informatica,Elettronica,Scooter Elettrici,Scooter,Elettrici,Colore Azzurro,Azzurro,","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spos",http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,,http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,,,,,,"2016-02-25 15:50:21",,,,,,,,,,,,,,,,,"GTIN=4899888109204",60,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87995.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87979.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87978.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87976.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87975.jpg","GTIN=4899888109204",,,,,,,,,, +BB-H1000174,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Scooter Elettrici",base,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Nero","<a id=""maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""><p>Dimentica</a> i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà. Metti alla prova il tuo equilibrio e divertiti!</p><p><br /><strong><a href=""http://www.roverdroid.com/"">www.roverdroid.com</a></strong></p><p>Caratteristiche:</p><ul><li>Batteria a litio: 160 Wh</li><li>Pulsante on/off</li><li>Indicatore luminoso del livello di carica sull'asse centrale</li><li>Illuminazione di segnaletica anteriore LED</li><li>Zona d'appoggio in gomma resistente e antiscivolo</li><li>Velocità massima: 10 km/h circa</li><li>Autonomia con carica completa: da 17 a 20 km circa</li><li>Peso: 10 kg circa</li><li>Tempo di carica: 3 ore circa</li><li>Peso massimo supportato: 100 kg</li><li>Dimensioni: 58 x 18 x 18 cm circa</li><li>Diametro delle ruote: 7""</li><li>Borsa di trasporto e caricabatterie inclusi (CA: 100-240 V, 50-60 Hz, 1,2 A / DC: 36 V, 2 A)</li></ul><p> Dimenzioni per Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid: </br><ul><li>Altezza: 22.5 Cm</li><li>Larghezza: 64 Cm</li><li>Profondita': 24 Cm</li><li>Peso: 11.85 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109273</p>","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà.</br><a href=""#maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""> Maggiori Informazioni</a>",11.85,1,"Taxable Goods","Catalog, Search",599,,,,Mini-Scooter-Elettrico-di-Auto-Equilibrio-(2-ruote)-Rover-Droid-Nero,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Nero","Informatica Elettronica,Informatica,Elettronica,Scooter Elettrici,Scooter,Elettrici,Colore Nero,Nero,","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spos",http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,,http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,,,,,,"2016-02-25 15:50:21",,,,,,,,,,,,,,,,,"GTIN=4899888109273",131,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87995.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87979.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87978.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87976.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87975.jpg","GTIN=4899888109273",,,,,,,,,, +BB-H1000182,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Scooter Elettrici",base,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Grafitti","<a id=""maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""><p>Dimentica</a> i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà. Metti alla prova il tuo equilibrio e divertiti!</p><p><br /><strong><a href=""http://www.roverdroid.com/"">www.roverdroid.com</a></strong></p><p>Caratteristiche:</p><ul><li>Batteria a litio: 160 Wh</li><li>Pulsante on/off</li><li>Indicatore luminoso del livello di carica sull'asse centrale</li><li>Illuminazione di segnaletica anteriore LED</li><li>Zona d'appoggio in gomma resistente e antiscivolo</li><li>Velocità massima: 10 km/h circa</li><li>Autonomia con carica completa: da 17 a 20 km circa</li><li>Peso: 10 kg circa</li><li>Tempo di carica: 3 ore circa</li><li>Peso massimo supportato: 100 kg</li><li>Dimensioni: 58 x 18 x 18 cm circa</li><li>Diametro delle ruote: 7""</li><li>Borsa di trasporto e caricabatterie inclusi (CA: 100-240 V, 50-60 Hz, 1,2 A / DC: 36 V, 2 A)</li></ul><p> Dimenzioni per Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid: </br><ul><li>Altezza: 22.5 Cm</li><li>Larghezza: 64 Cm</li><li>Profondita': 24 Cm</li><li>Peso: 11.85 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109747</p>","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà.</br><a href=""#maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""> Maggiori Informazioni</a>",11.85,1,"Taxable Goods","Catalog, Search",599,,,,Mini-Scooter-Elettrico-di-Auto-Equilibrio-(2-ruote)-Rover-Droid-Grafitti,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Grafitti","Informatica Elettronica,Informatica,Elettronica,Scooter Elettrici,Scooter,Elettrici,Colore Grafitti,Grafitti,","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spos",http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,,http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,,,,,,"2016-02-25 16:33:57",,,,,,,,,,,,,,,,,"GTIN=4899888109747",92,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87995.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87979.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87978.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87976.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87975.jpg","GTIN=4899888109747",,,,,,,,,, +BB-I2500322,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Smartwatch",base,"Orologio Intelligente Smartwatch BT110 con Audio Nero","<a id=""maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""><p>Sfoggia</a> il tuo <strong>orologio intelligente Smartwatch BT110 con audio</strong>! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro. Inoltre, questo orologio dispone di varie funzioni autonome: allarme, calendario, calcolatrice, controllo del sonno...</p><p><a href=""http://www.bitblin.com/"" target=""_blank""><strong>www.bitblin.com</strong></a><br /> <br />Caratteristiche:</p><ul><li>Schermo touch</li><li>Risoluzione: 128 x 128 pixel</li><li>Batteria a litio: 3,7 V / 230 mA</li><li>Durata appross. in attesa: 150 h</li><li>Durata appross. in conversazione: 3 h</li><li>Connessione micro USB</li><li>Bluetooth 3.0</li><li>Vibrazione</li><li>Dimensioni appross. della sfera: 4 x 4,5 x 1 cm</li><li>Cavo USB da micro USB incluso</li><li>Il menu è disponibile in spagnolo, inglese, francese, danese, polacco, portoghese, italiano, tedesco, turco, russo e svedese.</li><li>Compatibile con smartphones Android</li></ul><p> </p><p>Con Smartphone Android da 4.0 a 5.0 è possibile ricevere notifiche di SMS, e-mails, WhatsApp e social network (scaricando l'applicazione indicata nella pagina web del prodotto).</p><p> Dimenzioni per Orologio Intelligente Smartwatch BT110 con Audio: </br><ul><li>Altezza: 11 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.145 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109242</p>","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro.</br><a href=""#maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""> Maggiori Informazioni</a>",0.145,1,"Taxable Goods","Catalog, Search",94.9,,,,Orologio-Intelligente-Smartwatch-BT110-con-Audio-Nero,"Orologio Intelligente Smartwatch BT110 con Audio Nero","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Smartwatch,Colore Nero,Nero,","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usa",http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,,,,,,"2016-02-11 08:36:39",,,,,,,,,,,,,,,,,"GTIN=4899888109242",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101194.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101193.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84129.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84128.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84127.jpg","GTIN=4899888109242",,,,,,,,,, +BB-I2500323,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Smartwatch",base,"Orologio Intelligente Smartwatch BT110 con Audio Bianco","<a id=""maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""><p>Sfoggia</a> il tuo <strong>orologio intelligente Smartwatch BT110 con audio</strong>! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro. Inoltre, questo orologio dispone di varie funzioni autonome: allarme, calendario, calcolatrice, controllo del sonno...</p><p><a href=""http://www.bitblin.com/"" target=""_blank""><strong>www.bitblin.com</strong></a><br /> <br />Caratteristiche:</p><ul><li>Schermo touch</li><li>Risoluzione: 128 x 128 pixel</li><li>Batteria a litio: 3,7 V / 230 mA</li><li>Durata appross. in attesa: 150 h</li><li>Durata appross. in conversazione: 3 h</li><li>Connessione micro USB</li><li>Bluetooth 3.0</li><li>Vibrazione</li><li>Dimensioni appross. della sfera: 4 x 4,5 x 1 cm</li><li>Cavo USB da micro USB incluso</li><li>Il menu è disponibile in spagnolo, inglese, francese, danese, polacco, portoghese, italiano, tedesco, turco, russo e svedese.</li><li>Compatibile con smartphones Android</li></ul><p> </p><p>Con Smartphone Android da 4.0 a 5.0 è possibile ricevere notifiche di SMS, e-mails, WhatsApp e social network (scaricando l'applicazione indicata nella pagina web del prodotto).</p><p> Dimenzioni per Orologio Intelligente Smartwatch BT110 con Audio: </br><ul><li>Altezza: 11 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.145 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109259</p>","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro.</br><a href=""#maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""> Maggiori Informazioni</a>",0.145,1,"Taxable Goods","Catalog, Search",94.9,,,,Orologio-Intelligente-Smartwatch-BT110-con-Audio-Bianco,"Orologio Intelligente Smartwatch BT110 con Audio Bianco","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Smartwatch,Colore Bianco,Bianco,","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usa",http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,,,,,,"2016-02-11 08:36:39",,,,,,,,,,,,,,,,,"GTIN=4899888109259",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101194.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101193.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84129.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84128.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84127.jpg","GTIN=4899888109259",,,,,,,,,, +BB-I2500324,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Smartwatch",base,"Orologio Intelligente Smartwatch BT110 con Audio Rosso","<a id=""maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""><p>Sfoggia</a> il tuo <strong>orologio intelligente Smartwatch BT110 con audio</strong>! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro. Inoltre, questo orologio dispone di varie funzioni autonome: allarme, calendario, calcolatrice, controllo del sonno...</p><p><a href=""http://www.bitblin.com/"" target=""_blank""><strong>www.bitblin.com</strong></a><br /> <br />Caratteristiche:</p><ul><li>Schermo touch</li><li>Risoluzione: 128 x 128 pixel</li><li>Batteria a litio: 3,7 V / 230 mA</li><li>Durata appross. in attesa: 150 h</li><li>Durata appross. in conversazione: 3 h</li><li>Connessione micro USB</li><li>Bluetooth 3.0</li><li>Vibrazione</li><li>Dimensioni appross. della sfera: 4 x 4,5 x 1 cm</li><li>Cavo USB da micro USB incluso</li><li>Il menu è disponibile in spagnolo, inglese, francese, danese, polacco, portoghese, italiano, tedesco, turco, russo e svedese.</li><li>Compatibile con smartphones Android</li></ul><p> </p><p>Con Smartphone Android da 4.0 a 5.0 è possibile ricevere notifiche di SMS, e-mails, WhatsApp e social network (scaricando l'applicazione indicata nella pagina web del prodotto).</p><p> Dimenzioni per Orologio Intelligente Smartwatch BT110 con Audio: </br><ul><li>Altezza: 11 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.145 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109266</p>","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro.</br><a href=""#maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""> Maggiori Informazioni</a>",0.145,1,"Taxable Goods","Catalog, Search",94.9,,,,Orologio-Intelligente-Smartwatch-BT110-con-Audio-Rosso,"Orologio Intelligente Smartwatch BT110 con Audio Rosso","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Smartwatch,Colore Rosso,Rosso,","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usa",http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,,,,,,"2016-02-11 08:36:39",,,,,,,,,,,,,,,,,"GTIN=4899888109266",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101194.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101193.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84129.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84128.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84127.jpg","GTIN=4899888109266",,,,,,,,,, +BB-I4110022,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Smartphone MyWigo UNO 5'' Bianco","<a id=""maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""><p>Acquista</a> uno dei migliori <strong>telefoni cellulari sbloccati</strong> sul mercato in questo momento, lo <strong>smartphone MyWigo UNO</strong> <strong>5''</strong>! Include caricabatterie e cavetto USB al cavetto micro USB.</p><p>Caratteristiche:</p><ul><li>Schermo Vetro Ricurvo 5'' HD IPS 2.5D </li><li>Batteria a polimeri di litio: 2350 mAh</li><li>Fotocamera anteriore: 5 Mpx</li><li>Telecamera posteriore: 13 Mpx Sony, IMX 214 sensore con autofocus e flash LED</li><li>Processore: MTK6753 Octa Core a 1.3 GHz di 64 Bit</li><li>RAM: 2 GB DDR</li><li>Memoria interna: 32 GB (16 GB + 16 GB micro SD)</li><li>Dual SIM</li><li>Sistema operativo: Android Lollipop 5.1</li><li>Wi-Fi</li><li>Bluetooth 4.0 + HS</li><li>GPS</li><li>2G: GSM a 850/900/1800/1900 MHz</li><li>3G: WCDMA 900/2100 MHz</li><li>Tecnologia 4G (LTE) FDD 800/1800/2100/2600 MHz</li><li>Caricabatterie: AC 110-240 V, DC 5 V, 1000 mA</li><li>Dimensioni: 7 x 14 x 0,8 cm circa</li><li>Dimensioni dello schermo: 6 x 11 cm circa</li><li>Peso: 138 gr circa</li></ul><p> Dimenzioni per Smartphone MyWigo UNO 5'' : </br><ul><li>Altezza: 10.5 Cm</li><li>Larghezza: 18 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.347 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436533839565</p>","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB.</br><a href=""#maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""> Maggiori Informazioni</a>",0.347,1,"Taxable Goods","Catalog, Search",412.5,,,,Smartphone-MyWigo-UNO-5''-Bianco,"Smartphone MyWigo UNO 5'' Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB",http://dropshipping.bigbuy.eu/imgs/I4110021_87809.jpg,,http://dropshipping.bigbuy.eu/imgs/I4110021_87809.jpg,,,,,,"2015-12-16 14:44:27",,,,,,,,,,,,,,,,,"GTIN=8436533839565",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4110021_87813.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87812.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87811.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87810.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87808.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87807.jpg","GTIN=8436533839565",,,,,,,,,, +BB-I4110023,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Smartphone MyWigo UNO 5'' Nero","<a id=""maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""><p>Acquista</a> uno dei migliori <strong>telefoni cellulari sbloccati</strong> sul mercato in questo momento, lo <strong>smartphone MyWigo UNO</strong> <strong>5''</strong>! Include caricabatterie e cavetto USB al cavetto micro USB.</p><p>Caratteristiche:</p><ul><li>Schermo Vetro Ricurvo 5'' HD IPS 2.5D </li><li>Batteria a polimeri di litio: 2350 mAh</li><li>Fotocamera anteriore: 5 Mpx</li><li>Telecamera posteriore: 13 Mpx Sony, IMX 214 sensore con autofocus e flash LED</li><li>Processore: MTK6753 Octa Core a 1.3 GHz di 64 Bit</li><li>RAM: 2 GB DDR</li><li>Memoria interna: 32 GB (16 GB + 16 GB micro SD)</li><li>Dual SIM</li><li>Sistema operativo: Android Lollipop 5.1</li><li>Wi-Fi</li><li>Bluetooth 4.0 + HS</li><li>GPS</li><li>2G: GSM a 850/900/1800/1900 MHz</li><li>3G: WCDMA 900/2100 MHz</li><li>Tecnologia 4G (LTE) FDD 800/1800/2100/2600 MHz</li><li>Caricabatterie: AC 110-240 V, DC 5 V, 1000 mA</li><li>Dimensioni: 7 x 14 x 0,8 cm circa</li><li>Dimensioni dello schermo: 6 x 11 cm circa</li><li>Peso: 138 gr circa</li></ul><p> Dimenzioni per Smartphone MyWigo UNO 5'' : </br><ul><li>Altezza: 10.5 Cm</li><li>Larghezza: 18 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.347 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436533839558</p>","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB.</br><a href=""#maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""> Maggiori Informazioni</a>",0.347,1,"Taxable Goods","Catalog, Search",412.5,,,,Smartphone-MyWigo-UNO-5''-Nero,"Smartphone MyWigo UNO 5'' Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB",http://dropshipping.bigbuy.eu/imgs/I4110021_87813.jpg,,http://dropshipping.bigbuy.eu/imgs/I4110021_87813.jpg,,,,,,"2015-12-16 14:44:27",,,,,,,,,,,,,,,,,"GTIN=8436533839558",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4110021_87809.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87812.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87811.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87810.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87808.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87807.jpg","GTIN=8436533839558",,,,,,,,,, +BB-V1400101,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Tlink11 Bianco","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""><p>Se</a> sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il <strong>telefono cellulare </strong><strong>Thomson Tlink11 </strong>è ciò che stai cercando!</p><p>Caratteristiche:</p><ul><li>Telefono cellulare sbloccato</li><li>Schermo: 1.77"" 128 x 160, colori 65 K</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Dual SIM</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Connessione per auricolari (non inclusi)</li><li>Lista contatti: 200 nomi</li><li>Funzione SMS</li><li>Mani-libere</li><li>Batteria Li-ion 600 mAh</li><li>Include: batteria e caricabatterie</li><li>Dimensioni: 4.5 x 11 x 1 cm circa </li></ul><p> Dimenzioni per Telefono Cellulare Thomson Tlink11: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570047688</p>","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSchermo: 1.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",43.5,,,,Telefono-Cellulare-Thomson-Tlink11-Bianco,"Telefono Cellulare Thomson Tlink11 Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSche",http://dropshipping.bigbuy.eu/imgs/V1400100_91205.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400100_91205.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570047688",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400100_91204.jpg,http://dropshipping.bigbuy.eu/imgs/V1400100_91203.jpg","GTIN=3527570047688",,,,,,,,,, +BB-V1400102,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Tlink11 Nero","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""><p>Se</a> sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il <strong>telefono cellulare </strong><strong>Thomson Tlink11 </strong>è ciò che stai cercando!</p><p>Caratteristiche:</p><ul><li>Telefono cellulare sbloccato</li><li>Schermo: 1.77"" 128 x 160, colori 65 K</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Dual SIM</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Connessione per auricolari (non inclusi)</li><li>Lista contatti: 200 nomi</li><li>Funzione SMS</li><li>Mani-libere</li><li>Batteria Li-ion 600 mAh</li><li>Include: batteria e caricabatterie</li><li>Dimensioni: 4.5 x 11 x 1 cm circa </li></ul><p> Dimenzioni per Telefono Cellulare Thomson Tlink11: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570047596</p>","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSchermo: 1.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",43.5,,,,Telefono-Cellulare-Thomson-Tlink11-Nero,"Telefono Cellulare Thomson Tlink11 Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSche",http://dropshipping.bigbuy.eu/imgs/V1400100_91204.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400100_91204.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570047596",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400100_91205.jpg,http://dropshipping.bigbuy.eu/imgs/V1400100_91203.jpg","GTIN=3527570047596",,,,,,,,,, +BB-V1400104,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Serea51 Bianco","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""><p>In</a> arrivo il <strong>telefono cellulare</strong><strong> Thomson Serea51</strong><strong> </strong>per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!</p><p>Caratteristiche:</p><ul><li>Cellulare sbloccato</li><li>Schermo: 1,77"" 160 x 128, 65 K colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Tasti di grandi dimensioni</li><li>Luce LED</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 220 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5 x 11 x 1,4 cm circa</li></ul><p> Dimenzioni per Telefono Cellulare Thomson Serea51: </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 11.5 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.288 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046964</p>","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1,77" 160 x 128, 65 K coloriReti: GSM-EDGE 850/900/1800/1900 MHzTasto per le chiamate d'emergenzaTasti di grandi dimensioniLuce LEDFotocamera VGABluetoothMP3Radio FMMicro USBMicro SD (scheda non inclusa)Agenda: 250 vociFunzione SMS, MMSkit auricolare mani libereBatteria Li-ion 800 mAhDurata della batteria: 220 h in standby / 5,5 h in conversazioneInclude: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolariDimensioni (senza base): 5 x 11 x 1,4 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""> Maggiori Informazioni</a>",0.288,1,"Taxable Goods","Catalog, Search",85.9,,,,Telefono-Cellulare-Thomson-Serea51-Bianco,"Telefono Cellulare Thomson Serea51 Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1",http://dropshipping.bigbuy.eu/imgs/V1400103_91207.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400103_91207.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046964",42,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400103_91208.jpg,http://dropshipping.bigbuy.eu/imgs/V1400103_91206.jpg","GTIN=3527570046964",,,,,,,,,, +BB-V1400105,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Serea51 Nero","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""><p>In</a> arrivo il <strong>telefono cellulare</strong><strong> Thomson Serea51</strong><strong> </strong>per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!</p><p>Caratteristiche:</p><ul><li>Cellulare sbloccato</li><li>Schermo: 1,77"" 160 x 128, 65 K colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Tasti di grandi dimensioni</li><li>Luce LED</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 220 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5 x 11 x 1,4 cm circa</li></ul><p> Dimenzioni per Telefono Cellulare Thomson Serea51: </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 11.5 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.288 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046186</p>","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1,77" 160 x 128, 65 K coloriReti: GSM-EDGE 850/900/1800/1900 MHzTasto per le chiamate d'emergenzaTasti di grandi dimensioniLuce LEDFotocamera VGABluetoothMP3Radio FMMicro USBMicro SD (scheda non inclusa)Agenda: 250 vociFunzione SMS, MMSkit auricolare mani libereBatteria Li-ion 800 mAhDurata della batteria: 220 h in standby / 5,5 h in conversazioneInclude: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolariDimensioni (senza base): 5 x 11 x 1,4 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""> Maggiori Informazioni</a>",0.288,1,"Taxable Goods","Catalog, Search",85.9,,,,Telefono-Cellulare-Thomson-Serea51-Nero,"Telefono Cellulare Thomson Serea51 Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1",http://dropshipping.bigbuy.eu/imgs/V1400103_91208.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400103_91208.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046186",33,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400103_91207.jpg,http://dropshipping.bigbuy.eu/imgs/V1400103_91206.jpg","GTIN=3527570046186",,,,,,,,,, +BB-V1400107,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellularel Thomson Serea62 Nero","<a id=""maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""><p>Se</a> sei alla ricerca di un <strong>cellulare</strong> semplice ma dal design impeccabile, non puoi lasciarti sfuggire il <strong>telefono cellulare Thomson Serea62</strong>. È studiato anche per le persone anziane che vogliono avvicinarsi al mondo della <strong>telefonia mobile</strong>. </p><p>Caratteristiche:</p><ul><li>Schermo: 2,4"" 240 x 320, 262 K a colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Menù semplificato e facile da usare</li><li>Tasti di grandi dimensioni</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Luce LED</li><li>Micro USB</li><li>Micro SD (carta non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 480 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5,5 x 10,5 x 2 cm circa</li></ul><p> Dimenzioni per Telefono Cellularel Thomson Serea62: </br><ul><li>Altezza: 5.2 Cm</li><li>Larghezza: 11.3 Cm</li><li>Profondita': 18.6 Cm</li><li>Peso: 0.284 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046094</p>","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""> Maggiori Informazioni</a>",0.284,1,"Taxable Goods","Catalog, Search",99.5,,,,Telefono-Cellularel-Thomson-Serea62-Nero,"Telefono Cellularel Thomson Serea62 Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62",http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046094",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91212.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91211.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91210.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91209.jpg","GTIN=3527570046094",,,,,,,,,, +BB-V1400108,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellularel Thomson Serea62 Bianco","<a id=""maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""><p>Se</a> sei alla ricerca di un <strong>cellulare</strong> semplice ma dal design impeccabile, non puoi lasciarti sfuggire il <strong>telefono cellulare Thomson Serea62</strong>. È studiato anche per le persone anziane che vogliono avvicinarsi al mondo della <strong>telefonia mobile</strong>. </p><p>Caratteristiche:</p><ul><li>Schermo: 2,4"" 240 x 320, 262 K a colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Menù semplificato e facile da usare</li><li>Tasti di grandi dimensioni</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Luce LED</li><li>Micro USB</li><li>Micro SD (carta non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 480 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5,5 x 10,5 x 2 cm circa</li></ul><p> Dimenzioni per Telefono Cellularel Thomson Serea62: </br><ul><li>Altezza: 5.2 Cm</li><li>Larghezza: 11.3 Cm</li><li>Profondita': 18.6 Cm</li><li>Peso: 0.284 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046100</p>","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""> Maggiori Informazioni</a>",0.284,1,"Taxable Goods","Catalog, Search",99.5,,,,Telefono-Cellularel-Thomson-Serea62-Bianco,"Telefono Cellularel Thomson Serea62 Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62",http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046100",13,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91212.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91211.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91210.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91209.jpg","GTIN=3527570046100",,,,,,,,,, +BB-V1400109,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellularel Thomson Serea62 Rosso","<a id=""maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""><p>Se</a> sei alla ricerca di un <strong>cellulare</strong> semplice ma dal design impeccabile, non puoi lasciarti sfuggire il <strong>telefono cellulare Thomson Serea62</strong>. È studiato anche per le persone anziane che vogliono avvicinarsi al mondo della <strong>telefonia mobile</strong>. </p><p>Caratteristiche:</p><ul><li>Schermo: 2,4"" 240 x 320, 262 K a colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Menù semplificato e facile da usare</li><li>Tasti di grandi dimensioni</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Luce LED</li><li>Micro USB</li><li>Micro SD (carta non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 480 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5,5 x 10,5 x 2 cm circa</li></ul><p> Dimenzioni per Telefono Cellularel Thomson Serea62: </br><ul><li>Altezza: 5.2 Cm</li><li>Larghezza: 11.3 Cm</li><li>Profondita': 18.6 Cm</li><li>Peso: 0.284 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046117</p>","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""> Maggiori Informazioni</a>",0.284,1,"Taxable Goods","Catalog, Search",99.5,,,,Telefono-Cellularel-Thomson-Serea62-Rosso,"Telefono Cellularel Thomson Serea62 Rosso","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Rosso,Rosso,","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62",http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046117",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91212.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91211.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91210.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91209.jpg","GTIN=3527570046117",,,,,,,,,, +BB-V0100186,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Cuffie Fatina maginca Playz Kidz","<a id=""maggiorni_informazioni"" title=""Cuffie Fatina maginca Playz Kidz""><p>Le</a> <strong>cuffie Fatina Magica Playz Kidz </strong>son perfetti per i piccoli di casa! Queste <strong><strong>cuffie</strong> per bambini</strong> sono ideali come regalo per i re della casa!</p><p><a href=""http://www.playzkidz.com"" target=""_blank""><strong>www.playzkidz.com</strong></a></p><ul><li>Auricolari stereo</li><li>Cuffie imbottite</li><li>Compatibili con MP3, MP4, CD, radio e PC</li><li>Età raccomandata: +4 anni</li></ul><p> Dimenzioni per Cuffie Fatina maginca Playz Kidz: </br><ul><li>Altezza: 22.2 Cm</li><li>Larghezza: 9.5 Cm</li><li>Profondita': 26.7 Cm</li><li>Peso: 0.243 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888111122</p>","Le cuffie Fatina Magica Playz Kidz son perfetti per i piccoli di casa! Queste cuffie per bambini sono ideali come regalo per i re della casa!www.</br><a href=""#maggiorni_informazioni"" title=""Cuffie Fatina maginca Playz Kidz""> Maggiori Informazioni</a>",0.243,1,"Taxable Goods","Catalog, Search",18.9,,,,Cuffie-Fatina-maginca-Playz-Kidz,"Cuffie Fatina maginca Playz Kidz","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,","Le cuffie Fatina Magica Playz Kidz son perfetti per i piccoli di casa! Queste cuffie per bambini sono ideali come regalo per i re della casa!www",http://dropshipping.bigbuy.eu/imgs/V0100186_93443.jpg,,http://dropshipping.bigbuy.eu/imgs/V0100186_93443.jpg,,,,,,"2016-08-16 05:37:23",,,,,,,,,,,,,,,,,"GTIN=4899888111122",2436,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0100186_93379.jpg,http://dropshipping.bigbuy.eu/imgs/V0100186_93377.jpg,http://dropshipping.bigbuy.eu/imgs/V0100186_93376.jpg,http://dropshipping.bigbuy.eu/imgs/V0100186_93375.jpg","GTIN=4899888111122",,,,,,,,,, +BB-V0100187,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Cuffie Mostriciattoli Playz Kidz","<a id=""maggiorni_informazioni"" title=""Cuffie Mostriciattoli Playz Kidz""><p>I</a> piccoli di casa impazziranno per le <strong><strong>cuffie</strong> Mostriciattoli Playz Kidz</strong>! Grazie al loro design originale e divertente, queste <strong><strong>cuffie</strong> per bambini </strong>sono il regalo perfetto!</p><p><a href=""http://www.playzkidz.com"" target=""_blank""><strong>www.playzkidz.com</strong></a></p><ul><li>Auricolari stereo</li><li>Cuffie imbottite</li><li>Compatibili con MP3, MP4, CD, radio e PC</li><li>Età raccomandata: +4 anni</li></ul><p> Dimenzioni per Cuffie Mostriciattoli Playz Kidz: </br><ul><li>Altezza: 22.2 Cm</li><li>Larghezza: 9.5 Cm</li><li>Profondita': 26.7 Cm</li><li>Peso: 0.245 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888111139</p>","I piccoli di casa impazziranno per le cuffie Mostriciattoli Playz Kidz! Grazie al loro design originale e divertente, queste cuffie per bambini sono il regalo perfetto!www.</br><a href=""#maggiorni_informazioni"" title=""Cuffie Mostriciattoli Playz Kidz""> Maggiori Informazioni</a>",0.245,1,"Taxable Goods","Catalog, Search",18.9,,,,Cuffie-Mostriciattoli-Playz-Kidz,"Cuffie Mostriciattoli Playz Kidz","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,","I piccoli di casa impazziranno per le cuffie Mostriciattoli Playz Kidz! Grazie al loro design originale e divertente, queste cuffie per bambini sono il regalo perfetto!www",http://dropshipping.bigbuy.eu/imgs/V0100187_93370.jpg,,http://dropshipping.bigbuy.eu/imgs/V0100187_93370.jpg,,,,,,"2016-08-30 08:26:19",,,,,,,,,,,,,,,,,"GTIN=4899888111139",2438,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0100187_93374.jpg,http://dropshipping.bigbuy.eu/imgs/V0100187_93373.jpg,http://dropshipping.bigbuy.eu/imgs/V0100187_93372.jpg,http://dropshipping.bigbuy.eu/imgs/V0100187_93371.jpg","GTIN=4899888111139",,,,,,,,,, +BB-V1300174,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars R2-D2","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787128</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-R2-D2,"Orologio Sveglia con Contasecondi Star Wars R2-D2","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design R2-D2,R2-D2,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787128",69,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787128",,,,,,,,,, +BB-V1300175,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars Stormtrooper","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787135</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-Stormtrooper,"Orologio Sveglia con Contasecondi Star Wars Stormtrooper","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design Stormtrooper,Stormtrooper,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787135",69,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787135",,,,,,,,,, +BB-V1300176,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars Yoda","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787142</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-Yoda,"Orologio Sveglia con Contasecondi Star Wars Yoda","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design Yoda,Yoda,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787142",68,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787142",,,,,,,,,, +BB-V1300177,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars Chewbacca","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787159</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-Chewbacca,"Orologio Sveglia con Contasecondi Star Wars Chewbacca","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design Chewbacca,Chewbacca,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787159",69,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787159",,,,,,,,,, +BB-G1000110,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Offerte",base,"Attrezzo da Ginnastica Body Rocker","<a id=""maggiorni_informazioni"" title=""Attrezzo da Ginnastica Body Rocker ""><p>Non</a> perdere tempo e denaro andandoin <strong>palestra</strong> e procurarti ora l'<strong>attrezzo da ginnastica Body Rocker! </strong>Una forma facile ed efficace di rimettresi in forma facendo esercizio in casa. Grazie al suo sistema di oscillazione, è perfetto per lavorare e rafforzare, spalle, braccia, schiena, petto, glutei, ecc. Fatto in acciaio con impugnature in gomma. Include manuale d'istruzioni e DVD dimostrativo. (Questo prodotto può può presentare lievi danni che non impediscono il funzionamento del prodotto: impugnature in gomma piuma scollate).</p><p> Dimenzioni per Attrezzo da Ginnastica Body Rocker : </br><ul><li>Altezza: 21 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 78 Cm</li><li>Peso: 2.13 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101314</p>","Non perdere tempo e denaro andandoin palestra e procurarti ora l'attrezzo da ginnastica Body Rocker! Una forma facile ed efficace di rimettresi in forma facendo esercizio in casa.</br><a href=""#maggiorni_informazioni"" title=""Attrezzo da Ginnastica Body Rocker ""> Maggiori Informazioni</a>",2.13,1,"Taxable Goods","Catalog, Search",79,,,,Attrezzo-da-Ginnastica-Body-Rocker,"Attrezzo da Ginnastica Body Rocker","Outlet Offerte,Outlet,Offerte,Offerte,","Non perdere tempo e denaro andandoin palestra e procurarti ora l'attrezzo da ginnastica Body Rocker! Una forma facile ed efficace di rimettresi in forma facendo esercizio in casa",http://dropshipping.bigbuy.eu/imgs/bodirocker-00.jpg,,http://dropshipping.bigbuy.eu/imgs/bodirocker-00.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=4899888101314",121,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/bodirocker-03.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-06.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-05.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-07.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-02.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-01.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-04.jpg","GTIN=4899888101314",,,,,,,,,, +BB-J2000066,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Arancio","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Lovely Arancio,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Arancio","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Lovely Arancio,Lovely Arancio,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,,,,,,"2016-02-11 15:49:04",,,,,,,,,,,,,,,,,"GTIN=4899888102731",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000239,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Commando","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Commando,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Commando","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Commando,Commando,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,,,,,,"2016-02-11 15:49:04",,,,,,,,,,,,,,,,,"GTIN=4899888102731",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000240,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Galaktic","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Galaktic,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Galaktic","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Galaktic,Galaktic,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,,,,,,"2016-02-11 15:49:04",,,,,,,,,,,,,,,,,"GTIN=4899888102731",14,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000329,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Blu","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Lovely Blu,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Blu","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Lovely Blu,Lovely Blu,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888102731",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000085,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Rosa S","<a id=""maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""><p>Acquista</a> <strong>Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</strong> Scopri il modo più facile per mantenerti caldo a casa quando c'è freddo! Devi solo riscaldare questi <strong>stivali rilassanti nel microonde</strong> per godere immediatamente del calore e del rilassamento ai tuoi piedi. Sono fatti di morbido tessuto polare e di un rivestimento in semi di lavanda che emana un <strong>profumo straordinario</strong> quando riscaldato. Goditi semplicemente il profumo rilassante! I Warm Hug Feet Stivali Riscaldabili al Microonde mantengono il calore per lungo tempo grazie ai semi di lavanda contenuti all'interno. Per pulire questi stivali riscaldabili, passaci sopra un panno umido. Non riscaldarmi mai per più di 2 minuti e non usarli se sono troppo caldi. Disponibili in rosa e blu.</p><p>Taglie (equivalenze approssimative)</p><ul><li>S (35 a 38)</li><li>M (38 a 41)</li><li>L (41 a 43)</li></ul><p>Rispettare sempre i limiti nel microonde</p><ul><li>800W</li><li>80ºC</li><li>990 sec</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio): </br><ul><li>Altezza: 27 Cm</li><li>Larghezza: 10.5 Cm</li><li>Profondita': 29 Cm</li><li>Peso: 0.67 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899881028786</p>","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""> Maggiori Informazioni</a>",0.67,1,"Taxable Goods","Catalog, Search",33.5,,,,OUTLET-Warm-Hug-Feet-Stivali-Riscaldabili-al-Microonde-(Senza-imballaggio)-Rosa-S,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Rosa S","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Rosa,Rosa,Taglia S,S,","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,,,,,,"2016-02-02 10:45:18",,,,,,,,,,,,,,,,,"GTIN=4899881028786",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-02.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-04.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-05.jpg","GTIN=4899881028786",,,,,,,,,, +BB-J2000086,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola M","<a id=""maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""><p>Acquista</a> <strong>Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</strong> Scopri il modo più facile per mantenerti caldo a casa quando c'è freddo! Devi solo riscaldare questi <strong>stivali rilassanti nel microonde</strong> per godere immediatamente del calore e del rilassamento ai tuoi piedi. Sono fatti di morbido tessuto polare e di un rivestimento in semi di lavanda che emana un <strong>profumo straordinario</strong> quando riscaldato. Goditi semplicemente il profumo rilassante! I Warm Hug Feet Stivali Riscaldabili al Microonde mantengono il calore per lungo tempo grazie ai semi di lavanda contenuti all'interno. Per pulire questi stivali riscaldabili, passaci sopra un panno umido. Non riscaldarmi mai per più di 2 minuti e non usarli se sono troppo caldi. Disponibili in rosa e blu.</p><p>Taglie (equivalenze approssimative)</p><ul><li>S (35 a 38)</li><li>M (38 a 41)</li><li>L (41 a 43)</li></ul><p>Rispettare sempre i limiti nel microonde</p><ul><li>800W</li><li>80ºC</li><li>990 sec</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio): </br><ul><li>Altezza: 27 Cm</li><li>Larghezza: 10.5 Cm</li><li>Profondita': 29 Cm</li><li>Peso: 0.67 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899881028786</p>","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""> Maggiori Informazioni</a>",0.67,1,"Taxable Goods","Catalog, Search",33.5,,,,OUTLET-Warm-Hug-Feet-Stivali-Riscaldabili-al-Microonde-(Senza-imballaggio)-Viola-M,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola M","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Viola,Viola,Taglia M,M,","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,,,,,,"2016-02-02 10:45:18",,,,,,,,,,,,,,,,,"GTIN=4899881028786",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-02.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-04.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-05.jpg","GTIN=4899881028786",,,,,,,,,, +BB-J2000188,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola L","<a id=""maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""><p>Acquista</a> <strong>Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</strong> Scopri il modo più facile per mantenerti caldo a casa quando c'è freddo! Devi solo riscaldare questi <strong>stivali rilassanti nel microonde</strong> per godere immediatamente del calore e del rilassamento ai tuoi piedi. Sono fatti di morbido tessuto polare e di un rivestimento in semi di lavanda che emana un <strong>profumo straordinario</strong> quando riscaldato. Goditi semplicemente il profumo rilassante! I Warm Hug Feet Stivali Riscaldabili al Microonde mantengono il calore per lungo tempo grazie ai semi di lavanda contenuti all'interno. Per pulire questi stivali riscaldabili, passaci sopra un panno umido. Non riscaldarmi mai per più di 2 minuti e non usarli se sono troppo caldi. Disponibili in rosa e blu.</p><p>Taglie (equivalenze approssimative)</p><ul><li>S (35 a 38)</li><li>M (38 a 41)</li><li>L (41 a 43)</li></ul><p>Rispettare sempre i limiti nel microonde</p><ul><li>800W</li><li>80ºC</li><li>990 sec</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio): </br><ul><li>Altezza: 27 Cm</li><li>Larghezza: 10.5 Cm</li><li>Profondita': 29 Cm</li><li>Peso: 0.67 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899881028786</p>","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""> Maggiori Informazioni</a>",0.67,1,"Taxable Goods","Catalog, Search",33.5,,,,OUTLET-Warm-Hug-Feet-Stivali-Riscaldabili-al-Microonde-(Senza-imballaggio)-Viola-L,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola L","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Viola,Viola,Taglia L,L,","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,,,,,,"2016-02-02 10:45:18",,,,,,,,,,,,,,,,,"GTIN=4899881028786",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-02.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-04.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-05.jpg","GTIN=4899881028786",,,,,,,,,, +BB-J2000123,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)""><p>Avviso</a> per gli amanti del gelato: ecco la nuovissima <strong>macchina per gelato</strong> <strong>Princess 282602</strong>. Una <strong>gelatiera</strong> diversa dalle altre, che ti permetterà di preparare in un batter d'occhio gelati dolci o salati per i grandi e i più piccini!<strong> </strong>Al naturale o con salsa di frutta, pepite o qualsiasi altra guarnizione...le tue papille ti ringrazieranno! Indispensabile per un'estate al fresco!</p><p>Basta mettere in freezer, per 12 ore circa, il recipiente rimovibile a forma di secchiello con manico integrato di 17,5 x 14,5 cm (diametro x altezza) ed è fatto! Il gelato 100% naturale è servito!</p><p>Caratteristiche:</p><ul><li>Potenza: 5 W</li><li>Frequenza: 50 Hz</li><li>Tensione: 220-240 V</li><li>Pulsante On/Off </li><li>Gommini antiscivolo</li><li>Scomparto per cavo di alimentazione</li><li>Mescolatore rimovibile</li><li>Facile da pulire</li><li>Apertura di riempimento (dimensioni approssimative: 5 x 5,5 cm)</li><li>Dimensioni approssimative: 22 x 25 x 22 cm</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio): </br><ul><li>Altezza: 31 Cm</li><li>Larghezza: 23.4 Cm</li><li>Profondita': 23.5 Cm</li><li>Peso: 3.26 Kg</li></ul></p><p>Codice Prodotto (EAN): 8712836304895</p>","Avviso per gli amanti del gelato: ecco la nuovissima macchina per gelato Princess 282602.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)""> Maggiori Informazioni</a>",3.26,1,"Taxable Goods","Catalog, Search",110.88,,,,OUTLET-Macchina-per-Gelato-Princess-282602-(Senza-imballaggio),"OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","Avviso per gli amanti del gelato: ecco la nuovissima macchina per gelato Princess 282602",http://dropshipping.bigbuy.eu/imgs/J2000123_88610.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000123_88610.jpg,,,,,,"2016-08-09 01:54:36",,,,,,,,,,,,,,,,,"GTIN=8712836304895",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000123_88612.jpg,http://dropshipping.bigbuy.eu/imgs/J2000123_88611.jpg","GTIN=8712836304895",,,,,,,,,, +BB-J2000176,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Celeste","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Celeste,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Celeste","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Celeste,Celeste,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",5,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000178,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Rosso","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Rosso,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Rosso","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Rosso,Rosso,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",3,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000179,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cioccolato","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Cioccolato,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cioccolato","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Cioccolato,Cioccolato,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",5,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000180,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Crudo","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Crudo,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Crudo","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Crudo,Crudo,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",9,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000181,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Fragola","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Fragola,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Fragola","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Fragola,Fragola,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000182,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cardinale","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Cardinale,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cardinale","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Cardinale,Cardinale,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000247,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Turchese","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Turchese,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Turchese","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Turchese,Turchese,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",7,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000177,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)""><p>Hai</a> visto la <strong>macchina RC Ferrari 599 GTO</strong>? Puoi facilmente usare questo divertente giocattolo<strong> autorizzato Ferrari</strong> grazie al suo telecomando. La macchina funziona a batterie 5 x AA (non incluse) e il telecomando a batteria 1 x 6F22 9V (non inclusa). Giocattolo adatto per bambini di età superiore ai 6 anni. Funzioni della macchina radiocontrollata Ferrari:</p><ul><li>Avanti e indietro</li><li>Gira a sinistra e a destra</li><li>Fari e Fanali posteriori</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio): </br><ul><li>Altezza: 17.5 Cm</li><li>Larghezza: 43.5 Cm</li><li>Profondita': 22.7 Cm</li><li>Peso: 1.244 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158011299</p>","Hai visto la macchina RC Ferrari 599 GTO? Puoi facilmente usare questo divertente giocattolo autorizzato Ferrari grazie al suo telecomando.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)""> Maggiori Informazioni</a>",1.244,1,"Taxable Goods","Catalog, Search",119,,,,OUTLET-Macchina-RC-Ferrari-599-GTO--(Senza-imballaggio),"OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","Hai visto la macchina RC Ferrari 599 GTO? Puoi facilmente usare questo divertente giocattolo autorizzato Ferrari grazie al suo telecomando",http://dropshipping.bigbuy.eu/imgs/J2000177_89020.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000177_89020.jpg,,,,,,"2016-07-22 06:19:54",,,,,,,,,,,,,,,,,"GTIN=8718158011299",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000177_89022.jpg,http://dropshipping.bigbuy.eu/imgs/J2000177_89021.jpg","GTIN=8718158011299",,,,,,,,,, +BB-J2000255,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige S","<a id=""maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""><p>Metti</a> in luce un corpo scultoreo con la <strong>Canottiera Modellante con Reggiseno Booby & Tummy! </strong>Discreta, comod. Sostiene il seno e offre una grande capacità di sostegno. Prodotta il un tessuto delicato, flessibile e traspirante che si adatta perfettamente al tuo corpo e offre una copertura completa (seno, fianchi e glutei).  Equivalenza di taglie: S: 36-38, M: 38-40, L: 40-42.</p><p><a href=""http://www.boobyandtummy.com/"" target=""_blank""><strong>www.boobyandtummy.com</strong></a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 13.5 Cm</li><li>Profondita': 7 Cm</li><li>Peso: 0.14 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101352</p>","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""> Maggiori Informazioni</a>",0.14,1,"Taxable Goods","Catalog, Search",23.1,,,,OUTLET-Canottiera-Modellante-con-Reggiseno-Booby-&-Tummy--(Senza-imballaggio)-Beige-S,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige S","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Beige,Beige,Taglia S,S,","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod",http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,,,,,,"2016-02-12 08:36:52",,,,,,,,,,,,,,,,,"GTIN=4899888101352",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89904.jpg","GTIN=4899888101352",,,,,,,,,, +BB-J2000285,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige L","<a id=""maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""><p>Metti</a> in luce un corpo scultoreo con la <strong>Canottiera Modellante con Reggiseno Booby & Tummy! </strong>Discreta, comod. Sostiene il seno e offre una grande capacità di sostegno. Prodotta il un tessuto delicato, flessibile e traspirante che si adatta perfettamente al tuo corpo e offre una copertura completa (seno, fianchi e glutei).  Equivalenza di taglie: S: 36-38, M: 38-40, L: 40-42.</p><p><a href=""http://www.boobyandtummy.com/"" target=""_blank""><strong>www.boobyandtummy.com</strong></a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 13.5 Cm</li><li>Profondita': 7 Cm</li><li>Peso: 0.14 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101352</p>","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""> Maggiori Informazioni</a>",0.14,1,"Taxable Goods","Catalog, Search",23.1,,,,OUTLET-Canottiera-Modellante-con-Reggiseno-Booby-&-Tummy--(Senza-imballaggio)-Beige-L,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige L","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Beige,Beige,Taglia L,L,","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod",http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888101352",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89904.jpg","GTIN=4899888101352",,,,,,,,,, +BB-J2000279,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige M","<a id=""maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""><p>Metti</a> in luce un corpo scultoreo con la <strong>Canottiera Modellante con Reggiseno Booby & Tummy! </strong>Discreta, comod. Sostiene il seno e offre una grande capacità di sostegno. Prodotta il un tessuto delicato, flessibile e traspirante che si adatta perfettamente al tuo corpo e offre una copertura completa (seno, fianchi e glutei).  Equivalenza di taglie: S: 36-38, M: 38-40, L: 40-42.</p><p><a href=""http://www.boobyandtummy.com/"" target=""_blank""><strong>www.boobyandtummy.com</strong></a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 13.5 Cm</li><li>Profondita': 7 Cm</li><li>Peso: 0.14 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101352</p>","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""> Maggiori Informazioni</a>",0.14,1,"Taxable Goods","Catalog, Search",23.1,,,,OUTLET-Canottiera-Modellante-con-Reggiseno-Booby-&-Tummy--(Senza-imballaggio)-Beige-M,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige M","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Beige,Beige,Taglia M,M,","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod",http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888101352",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89904.jpg","GTIN=4899888101352",,,,,,,,,, +BB-J2000264,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)""><p>La <strong>scopa</a> elettrica triangolare 360 Sweep</strong><strong> </strong>è perfetta per pulire in modo facile, rapido ed efficace. Grazie alla sua tecnologia innovativa, le setole ruotano automaticamente per rimuovere a fondo tutto lo sporco. Inoltre, questa <strong>scopa elettrica</strong> è leggera e facile da usare, il che la rende molto comoda e pratica. La scopa elettrica Sweep ruota di 360º e raggiunge facilmente qualsiasi angolo di casa.</p><p>Prova la nuova scopa elettrica triangolare 360 Sweep e scopri un modo migliore per pulire!</p><p>Caratteristiche della Scopa elettrica triangolare 360 Sweep:</p><ul><li>Scopa elettrica senza fili</li><li>Setole rotanti</li><li>Bastone in alluminio removibile (c.ca 115cm)</li><li>Base piatta triangolare (3 lati identici: circa 33cm di lunghezza x 3cm di altezza)</li><li>Batteria ricaricabile  7.2V</li><li>Caricabatteria (230V, 50Hz)</li><li>Durata batteria: Approx. 30 min</li><li>Scopartimento interno raccogli-polvere</li><li>Leggera, facile da usare, svuotare e pulire</li><li>Estremamente silenziosa</li></ul><p> <a title=""Escoba Eléctrica Sweep 360"" href=""http://www.360sweep.com/"" target=""_blank"">www.360sweep.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio): </br><ul><li>Altezza: 32 Cm</li><li>Larghezza: 39.5 Cm</li><li>Profondita': 9.5 Cm</li><li>Peso: 1.625 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102458</p>","La scopa elettrica triangolare 360 Sweep è perfetta per pulire in modo facile, rapido ed efficace.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)""> Maggiori Informazioni</a>",1.625,1,"Taxable Goods","Catalog, Search",89.9,,,,OUTLET-Scopa-elettrica-triangolare-senza-fili-360-Sweep-(Senza-imballaggio),"OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","La scopa elettrica triangolare 360 Sweep è perfetta per pulire in modo facile, rapido ed efficace",http://dropshipping.bigbuy.eu/imgs/J2000264_89527.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000264_89527.jpg,,,,,,"2016-09-01 06:15:11",,,,,,,,,,,,,,,,,"GTIN=4899888102458",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000264_89533.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89532.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89531.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89530.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89529.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89528.jpg","GTIN=4899888102458",,,,,,,,,, +BB-J2000291,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)""><p>Porta</a> la spiaggia a casa con la <strong>sabbia kinetic per bambini Playz Kidz</strong>! Il regalo perfetto per sorprendere i tuoi piccoli, che si divertiranno a costruire ogni tipo di castello e forma di sabbia. Questo divertente ed originale gioco sviluppa i talenti artistici e la creatività dei bambini. Comprende circa 0,5 kg di sabbia kinetic e 3 accessori in plastica: un rullo, una forcella e una spatola. Non tossico. Non macchia i vestiti o si attacca alle mani. Età consigliata: 3+ anni</p><p><strong><a href=""http://www.playzkidz.com"">www.playzkidz.com</a></strong></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 14 Cm</li><li>Peso: 0.65 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888108085</p>","Porta la spiaggia a casa con la sabbia kinetic per bambini Playz Kidz! Il regalo perfetto per sorprendere i tuoi piccoli, che si divertiranno a costruire ogni tipo di castello e forma di sabbia.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)""> Maggiori Informazioni</a>",0.65,1,"Taxable Goods","Catalog, Search",11.9,,,,OUTLET-Sabbia-Kinetic-per-Bambini-Playz-Kidz--(Senza-imballaggio),"OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","Porta la spiaggia a casa con la sabbia kinetic per bambini Playz Kidz! Il regalo perfetto per sorprendere i tuoi piccoli, che si divertiranno a costruire ogni tipo di castello e forma di sabbia",http://dropshipping.bigbuy.eu/imgs/J2000291_90163.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000291_90163.jpg,,,,,,"2016-08-09 01:48:11",,,,,,,,,,,,,,,,,"GTIN=4899888108085",778,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000291_90165.jpg,http://dropshipping.bigbuy.eu/imgs/J2000291_90164.jpg","GTIN=4899888108085",,,,,,,,,, +BB-J2000350,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio) S","<a id=""maggiorni_informazioni"" title=""OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio)""><p><strong>Acquista</a> il Reggiseno Crochet </strong>(3 pezzi)<strong> al miglior prezzo. </strong>Sentiti fantastica e sexy per tutto il giorno! Dimentica i fili, fascette o spalline. Il Crochet Bra utilizza la tecnologia <em>Woven Everlast</em> per un comfort massimo. Questi reggiseni sono stati elaborati senza tutti quegli elementi in modo da essere usato comodamente dimenticando che lo stai indossando. Questo reggiseno si adatta al tuo seno, sollevandolo e sostenendolo. Il Crochet Bra si adatta perfettamente al tuo corpo e forma , senza lasciare segni o pieghe. In più è morbido, flessibile e molto comodo, e si adatta ad ogni coppa, sollevando il tio seno in modo significativo.</p><p><a href=""http://www.crochetbra.com"" target=""_blank"">www.crochetbra.com</a><br /><strong><br />Caratteristiche:</strong></p><ul><li>Lavabile in lavatrice</li><li>Bretelle comode</li><li>Fatto al 96% di nylon e 4% di spandex</li><li>Adattabile alla forma del tuo seno</li><li>Solleva il tuo seno</li><li>La confezione include 3 reggiseni (1 beige, 1 nero and 1 bianco)</li><li>Equivalenza taglie appross.: S: 80/85 - M: 90/95 - L: 100/105</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio): </br><ul><li>Altezza: 4.5 Cm</li><li>Larghezza: 15.5 Cm</li><li>Profondita': 27 Cm</li><li>Peso: 0.269 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101345</p>","Acquista il Reggiseno Crochet (3 pezzi) al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio)""> Maggiori Informazioni</a>",0.269,1,"Taxable Goods","Catalog, Search",34.9,,,,OUTLET-Reggiseno-Crochet-Bra-(3-Pezzi)-(Senza-imballaggio)-S,"OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio) S","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Taglia S,S,","Acquista il Reggiseno Crochet (3 pezzi) al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000349_93435.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000349_93435.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888101345",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000349_93438.jpg,http://dropshipping.bigbuy.eu/imgs/J2000349_93437.jpg,http://dropshipping.bigbuy.eu/imgs/J2000349_93436.jpg","GTIN=4899888101345",,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/BB-ProductsWorking.csv b/dev/tests/acceptance/tests/_data/BB-ProductsWorking.csv new file mode 100644 index 0000000000000..0ea052a043526 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/BB-ProductsWorking.csv @@ -0,0 +1,29 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +BB-D2010129,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Nero","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101772</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Nero,"Ventilatore Portatile Spray FunFan Nero","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Nero,Nero,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888101772",41,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888101772",,,,,,,,,, +BB-D2010130,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Bianco","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107965</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Bianco,"Ventilatore Portatile Spray FunFan Bianco","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Bianco,Bianco,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107965",741,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107965",,,,,,,,,, +BB-D2010131,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Rosso","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107972</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Rosso,"Ventilatore Portatile Spray FunFan Rosso","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Rosso,Rosso,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107972",570,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107972",,,,,,,,,, +BB-H1000163,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005922</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0592 Blu Marino,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0592 Blu Marino,CH0592 Blu Marino,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005922",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005922",,,,,,,,,, +BB-H1000162,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0596 Grigio","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005960</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0596 Grigio,"Sedia Pieghevole Campart Travel CH0596 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0596 Grigio,CH0596 Grigio,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005960",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005960",,,,,,,,,, +BB-F1520329,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005939</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0593 Blu Marino,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0593 Blu Marino,CH0593 Blu Marino,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005939",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005939",,,,,,,,,, +BB-F1520328,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005977</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0597 Grigio,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0597 Grigio,CH0597 Grigio,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005977",7,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005977",,,,,,,,,, +BB-H4502058,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Star Wars","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Star Wars""><p>I</a> fan di Star Wars non potranno fare a meno di appendere l'<strong>orologio da parete Star Wars</strong> in casa loro! Realizzato in plastica. Funziona a batterie (1 x AA, non incluse). Diametro circa: 25,5 cm. Spessore circa: 3,5 cm.</p><p> Dimenzioni per Orologio da Parete Star Wars: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 3.8 Cm</li><li>Peso: 0.287 Kg</li></ul></p><p>Codice Prodotto (EAN): 6950687214204</p>","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Star Wars""> Maggiori Informazioni</a>",0.287,1,"Taxable Goods","Catalog, Search",22.5,,,,Orologio-da-Parete-Star-Wars,"Orologio da Parete Star Wars","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica",http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=6950687214204",130,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H4502058_84713.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84711.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84710.jpg","GTIN=6950687214204",,,,,,,,,, +BB-G0500195,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Rosso","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Rosso,"Braccialetto Sportivo a LED MegaLed Rosso","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Rosso,Rosso,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-G0500196,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Verde","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Verde,"Braccialetto Sportivo a LED MegaLed Verde","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Verde,Verde,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",37,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-I2500333,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Mom's Diner","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""><p>Decora</a> la tua cucina con l'originale <strong>orologio da parete</strong> <strong>Mom's Diner</strong> in stile vintage! È realizzato in legno. Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Mom's Diner: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345052</p>","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Mom's-Diner,"Orologio da Parete Mom's Diner","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno",http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,,,,,"2016-07-21 13:05:12",,,,,,,,,,,,,,,,,"GTIN=4029811345052",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500333_88060.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88059.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88058.jpg","GTIN=4029811345052",,,,,,,,,, +BB-I2500334,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Coffee Endless Cup","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""><p>Se</a> sei un appassionato di caffè, non puoi rimanere senza l'<strong>orologio da parete Coffee Endless Cup</strong>! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Coffee Endless Cup: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345069</p>","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Coffee-Endless-Cup,"Orologio da Parete Coffee Endless Cup","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa",http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,,,,,"2016-08-30 13:41:52",,,,,,,,,,,,,,,,,"GTIN=4029811345069",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500334_88064.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88063.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88062.jpg","GTIN=4029811345069",,,,,,,,,, +BB-V0000252,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Stop!","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346196</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Stop!,"Insegna Dito Vintage Look Stop!","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Stop!,Stop!,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346196",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346196",,,,,,,,,, +BB-V0000253,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Adults Only","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346202</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Adults Only,"Insegna Dito Vintage Look Adults Only","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Adults Only,Adults Only,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346202",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346202",,,,,,,,,, +BB-V0000254,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Talk","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346318</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Talk,"Insegna Dito Vintage Look Talk","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Talk,Talk,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346318",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346318",,,,,,,,,, +BB-V0000256,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Go Left","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346325</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Go Left,"Freccia Decorativa Vintage Look Go Left","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Go Left,Go Left,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346325",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346325",,,,,,,,,, +BB-V0000257,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Exit","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346332</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Exit,"Freccia Decorativa Vintage Look Exit","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Exit,Exit,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346332",19,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346332",,,,,,,,,, +BB-V0000258,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Cold beer here","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346349</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Cold beer here,"Freccia Decorativa Vintage Look Cold beer here","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Cold beer here,Cold beer here,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346349",20,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346349",,,,,,,,,, +BB-V0200190,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Bianco","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Bianco,"Ciotola in Bambù TakeTokio Bianco","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Bianco,Bianco,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200192,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Grigio","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Grigio,"Ciotola in Bambù TakeTokio Grigio","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Grigio,Grigio,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",26,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200191,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Nero","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Nero,"Ciotola in Bambù TakeTokio Nero","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Nero,Nero,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200360,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Rosa","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Rosa,"Scatola porta Tè Flower Vintage Coconut Rosa","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Rosa,Rosa,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",13,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200361,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Azzurro","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Azzurro,"Scatola porta Tè Flower Vintage Coconut Azzurro","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Azzurro,Azzurro,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200353,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Barbecue",base,"Ventilatore a Pistola classico per Barbecue BBQ Classics","<a id=""maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""><p>Utilizza</a> i migliori barbecue alimentando il fuoco con il <strong>ventilatore a pistola classico per babecue BBQ Classics</strong>! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</p><p><a href=""http://www.bbqclassics.com"" target=""_blank""><strong>www.bbqclassics.com</strong></a></p><ul><li>Realizzato in plastica e metallo</li><li>Dimensioni: 25 x 18 x 4 cm circa</li></ul><p> Dimenzioni per Ventilatore a Pistola classico per Barbecue BBQ Classics: </br><ul><li>Altezza: 5.5 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 22 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158032706</p>","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",9.3,,,,Ventilatore-a-Pistola-classico-per-Barbecue-BBQ-Classics,"Ventilatore a Pistola classico per Barbecue BBQ Classics","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Barbecue,","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria",http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,,,,,"2016-09-13 09:56:07",,,,,,,,,,,,,,,,,"GTIN=8718158032706",60,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200353_92696.jpg,http://dropshipping.bigbuy.eu/imgs/V0200353_92694.jpg","GTIN=8718158032706",,,,,,,,,, +BB-V1600123,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""><p>Non</a> c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente. Con il <strong>contenitore portagiochi Frozen (32 x 23 cm)</strong> sarà semplicissimo!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni aprossimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> </p><p> Dimenzioni per Contenitore Portagiochi Frozen (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766006</p>","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Frozen-(32-x-23-cm),"Contenitore Portagiochi Frozen (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente",http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766006",48,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600123_93005.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93004.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93003.jpg","GTIN=8412842766006",,,,,,,,,, +BB-V1600124,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""><p>Desideri</a> sorprendere i più piccini con un regalo molto originale? Il <strong>contenitore portagiochi Spiderman (32 x 23 cm)</strong> decorerà e metterà in ordine le loro camerette.</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni approssimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766037</p>","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Spiderman--(32-x-23-cm),"Contenitore Portagiochi Spiderman (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette",http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766037",52,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600124_93008.jpg,http://dropshipping.bigbuy.eu/imgs/V1600124_93007.jpg","GTIN=8412842766037",,,,,,,,,, +BB-V1600125,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""><p>Insegna</a> ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del <strong>contenitore portagiochi Frozen (45 x 32 cm)</strong>. Il <strong>portagiocattoli</strong> che tutte le bambine sognano!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Frozen (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766129</p>","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Frozen-(45-x-32-cm),"Contenitore Portagiochi Frozen (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766129",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600125_93011.jpg,http://dropshipping.bigbuy.eu/imgs/V1600125_93009.jpg","GTIN=8412842766129",,,,,,,,,, +BB-V1600126,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""><p>I</a> piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> Spiderman</strong><strong> (45 x 32 cm)</strong>. Il <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> </strong>preferito dai bambini!</p><ul><li>Fabbricato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età raccomandata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766150</p>","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Spiderman-(45-x-32-cm),"Contenitore Portagiochi Spiderman (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766150",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600126_93014.jpg,http://dropshipping.bigbuy.eu/imgs/V1600126_93013.jpg","GTIN=8412842766150",,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/catalog_import_products_url_rewrite.csv b/dev/tests/acceptance/tests/_data/catalog_import_products_url_rewrite.csv new file mode 100644 index 0000000000000..95e359ce7a1d9 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_import_products_url_rewrite.csv @@ -0,0 +1,2 @@ +sku,url_key +SimpleProductForTest1,SimpleProductAfterImport1-new diff --git a/dev/tests/acceptance/tests/_data/catalog_product_err_img.csv b/dev/tests/acceptance/tests/_data/catalog_product_err_img.csv new file mode 100644 index 0000000000000..97ac55e8e5a20 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_product_err_img.csv @@ -0,0 +1,11 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,additional_images +simple1,,Default,simple,,base,simple1,,,,1,Taxable Goods,"Catalog, Search",100,,,,simple1,test.jpg +simple2,,Default,simple,,base,simple2,,,,2,Taxable Goods,"Catalog, Search",101,,,,simple2,test.jpg +simple3,,Default,simple,,base,simple3,,,,3,Taxable Goods,"Catalog, Search",102,,,,simple3,test.jpg +simple4,,Default,simple,,base,simple4,,,,4,Taxable Goods,"Catalog, Search",103,,,,simple4,test.jpg +simple5,,Default,simple,,base,simple5,,,,5,Taxable Goods,"Catalog, Search",104,,,,simple5,test.jpg +simple6,,Default,simple,,base,simple6,,,,6,Taxable Goods,"Catalog, Search",105,,,,simple6,test.jpg +simple7,,Default,simple,,base,simple7,,,,7,Taxable Goods,"Catalog, Search",106,,,,simple7,test.jpg +simple8,,Default,simple,,base,simple8,,,,8,Taxable Goods,"Catalog, Search",107,,,,simple8,test.jpg +simple9,,Default,simple,,base,simple9,,,,9,Taxable Goods,"Catalog, Search",108,,,,simple9,test.jpg +simple10,,Default,simple,,base,simple10,,,,10,Taxable Goods,"Catalog, Search",109,,,,simple10,test.jpg diff --git a/dev/tests/acceptance/tests/_data/export_import_configurable_product.csv b/dev/tests/acceptance/tests/_data/export_import_configurable_product.csv new file mode 100644 index 0000000000000..97e63d06abe71 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/export_import_configurable_product.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,associated_skus,configurable_variations,configurable_variation_labels +api-configurable-export-import-product,,Default,configurable,Default Category/CategoryExportImport,base,API Configurable Export Import Product,,,2,1,Taxable Goods,"Catalog, Search",123,,,,api-configurable-export-import-product,,,,/m/a/magento-logo_1.png,Magento Logo,/m/a/magento-logo_1.png,Magento Logo,/m/a/magento-logo_1.png,Magento Logo,,,"7/26/19, 8:21 AM","7/26/19, 8:21 AM",,,Block after Info Column,,,,,,,,,,,Use config,,,0,0,1,0,0,1,1,1,0,1,1,,1,0,1,1,0,1,0,0,0,,,,,,,,,,,,,,,,,,"sku=api-simple-one-export-import,attribute=option1|sku=api-simple-two-export-import,attribute=option2",attribute=attributeExportImport diff --git a/dev/tests/acceptance/tests/_data/importSpecChars.csv b/dev/tests/acceptance/tests/_data/importSpecChars.csv new file mode 100644 index 0000000000000..5d62286ccf2ee --- /dev/null +++ b/dev/tests/acceptance/tests/_data/importSpecChars.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +Mug,,Default,simple,Default Category/C1,base,Mug,<p>this is a mug</p>,,,1,Taxable Goods,"Catalog, Search",30,,,,mug,Mug,Mug,Mug ,,,,,,,,,"10/1/18, 9:21 PM","10/1/18, 11:30 PM",,,Block after Info Column,,,,Use config,,,,,,,,,gift_wrapping_available=Use config,99,0,1,0,0,1,1,1,10000,1,1,1,1,1,1,1,1,1,0,0,0,1,1,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/import_productsoftwostoresdata.csv b/dev/tests/acceptance/tests/_data/import_productsoftwostoresdata.csv new file mode 100644 index 0000000000000..5cb120e7e2b2b --- /dev/null +++ b/dev/tests/acceptance/tests/_data/import_productsoftwostoresdata.csv @@ -0,0 +1,7 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,msrp_price,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id +name4,,Default,simple,,base,name4,name4,name4,0,1,None,Catalog,39,1,7/8/2015 8:00,,name4,,,,12/16/2015 6:33,7/7/2016 13:01,,,Product Info Column,,,,,,,,Use config,,,1,0,1,0,0,0,1,1,10000,1,1,1,1,1,1,1,0,1,0,0,1 +name4,english,Default,simple,,base,, ,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +name4,chinese,Default,simple,,base,白瓷奶勺110厘米, ,白瓷奶勺110厘米,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +name5,,Default,simple,,base,name5,name5,name5,0,1,,Catalog,229,111.75,7/15/2015 0:00,,name5,,,,12/16/2015 6:33,7/7/2016 13:01,,,Product Info Column,,,,,,,,Use config,,,0,0,1,0,2,2,1,1,10000,1,1,1,1,1,1,1,0,1,0,0,1 +name5,chinese,Default,simple,,base,盐磨瓶18厘米,,盐磨瓶18厘米,,2,None,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +name5,english,Default,simple,,base,,,,,2,None,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/import_simple_product.csv b/dev/tests/acceptance/tests/_data/import_simple_product.csv new file mode 100644 index 0000000000000..b7eea288db4df --- /dev/null +++ b/dev/tests/acceptance/tests/_data/import_simple_product.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,use_config_is_redeemable,use_config_lifetime,use_config_allow_message,use_config_email_template,associated_skus,configurable_variations,configurable_variation_labels +test_sku,,Default,simple,Default Category/simpleCategory5d53a993b7ccb2,base,Test_Product,,,,1,Taxable Goods,"Catalog, Search",560,,,,test-product,Test_Product,Test_Product,Test_Product ,,,,,,,,,"8/14/19, 6:27 AM","8/14/19, 6:27 AM",,,Block after Info Column,,,,Use config,,,,,,,Use config,,,25,0,1,0,0,1,1,1,10000,1,1,1,1,1,1,1,1,1,0,0,0,1,1,,,,,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/simpleProductUpdate.csv b/dev/tests/acceptance/tests/_data/simpleProductUpdate.csv new file mode 100644 index 0000000000000..147d6f8ade275 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/simpleProductUpdate.csv @@ -0,0 +1,2 @@ +sku,url_key +simpleProduct,simpleProd diff --git a/dev/tests/acceptance/tests/_data/tablerates.csv b/dev/tests/acceptance/tests/_data/tablerates.csv new file mode 100644 index 0000000000000..ddc591798e3cc --- /dev/null +++ b/dev/tests/acceptance/tests/_data/tablerates.csv @@ -0,0 +1,21 @@ +Country,Region/State,Zip/Postal Code,Weight (and above),Shipping Price +ASM,*,*,0,9.95 +FSM,*,*,0,9.95 +GUM,*,*,0,9.95 +MHL,*,*,0,9.95 +MNP,*,*,0,9.95 +PLW,*,*,0,9.95 +USA,AA,*,0,9.95 +USA,AE,*,0,9.95 +USA,AK,*,0,9.95 +USA,AP,*,0,9.95 +USA,AS,*,0,9.95 +USA,FM,*,0,9.95 +USA,GU,*,0,9.95 +USA,HI,*,0,9.95 +USA,MH,*,0,9.95 +USA,MP,*,0,9.95 +USA,PR,*,0,9.95 +USA,PW,*,0,9.95 +USA,VI,*,0,9.95 +VIR,*,*,0,9.95 \ No newline at end of file diff --git a/dev/tests/acceptance/tests/_data/usa_tablerates.csv b/dev/tests/acceptance/tests/_data/usa_tablerates.csv new file mode 100644 index 0000000000000..d5a59ae6bccf2 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/usa_tablerates.csv @@ -0,0 +1,13 @@ +Country,Region/State,"Zip/Postal Code","Order Subtotal (and above)","Shipping Price" +USA,*,*,0.0000,7.9900 +USA,*,*,7.0000,6.9900 +USA,*,*,13.0000,5.9900 +USA,*,*,25.9900,4.9900 +USA,AK,*,0.0000,8.9900 +USA,AK,*,7.0000,7.9900 +USA,AK,*,13.0000,6.9900 +USA,AK,*,25.9900,5.9900 +USA,HI,*,0.0000,8.9900 +USA,HI,*,7.0000,7.9900 +USA,HI,*,13.0000,6.9900 +USA,HI,*,25.9900,5.9900 diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml index f0bfec543f281..cac9d0c3cb55f 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml @@ -27,4 +27,23 @@ <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchGrabConfigProductPageImageSrc" after="searchAssertConfigProductPage"/> <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchGrabConfigProductPageImageSrc" stepKey="searchAssertConfigProductPageImageNotDefault" after="searchGrabConfigProductPageImageSrc"/> </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <!-- Search configurable product --> + <comment userInput="Search configurable product" stepKey="commentSearchConfigurableProduct" after="searchAssertSimpleProduct2ImageNotDefault" /> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="searchAssertFilterCategoryConfigProduct" after="commentSearchConfigurableProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="searchGrabConfigProductImageSrc" after="searchAssertFilterCategoryConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchGrabConfigProductImageSrc" stepKey="searchAssertConfigProductImageNotDefault" after="searchGrabConfigProductImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$)}}" stepKey="searchClickConfigProductView" after="searchAssertConfigProductImageNotDefault"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="searchAssertConfigProductPage" after="searchClickConfigProductView"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchGrabConfigProductPageImageSrc" after="searchAssertConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchGrabConfigProductPageImageSrc" stepKey="searchAssertConfigProductPageImageNotDefault" after="searchGrabConfigProductPageImageSrc"/> + </test> </tests> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/etc/module.xml new file mode 100644 index 0000000000000..bae0739d237e1 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/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_TestModuleCatalogSearch"> + <sequence> + <module name="Magento_CatalogSearch"/> + </sequence> + </module> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/registration.php new file mode 100644 index 0000000000000..78fb97a9e1134 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch', __DIR__); +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php b/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php index b3c3c124cfe47..9411523db0f2e 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php +++ b/dev/tests/api-functional/_files/Magento/TestModuleUps/Model/Carrier.php @@ -7,10 +7,10 @@ namespace Magento\TestModuleUps\Model; -use Magento\Framework\Async\ProxyDeferredFactory; use Magento\Framework\HTTP\AsyncClientInterface; use Magento\Framework\HTTP\ClientFactory; use Magento\Framework\Xml\Security; +use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory; use Magento\Ups\Helper\Config; /** diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php index a5be493032836..8061cb138660d 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -10,6 +10,7 @@ use Magento\Config\Model\Config; use Magento\Config\Model\ResourceModel\Config as ConfigResource; use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\TestCase; @@ -156,4 +157,31 @@ private function getStoreIdByCode(string $storeCode): int $store = $storeManager->getStore($storeCode); return (int)$store->getId(); } + + /** + * @inheritDoc + */ + protected function _setConfigValue($configPath, $value, $storeCode = false) + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + if ($storeCode === false) { + $objectManager->get( + \Magento\TestFramework\App\ApiMutableScopeConfig::class + )->setValue( + $configPath, + $value, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + + return; + } + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\TestFramework\App\ApiMutableScopeConfig::class + )->setValue( + $configPath, + $value, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeCode + ); + } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/App/MutableScopeConfig.php b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php similarity index 86% rename from dev/tests/api-functional/framework/Magento/TestFramework/App/MutableScopeConfig.php rename to dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php index efcb5be34e594..fa0cebece9a96 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/App/MutableScopeConfig.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php @@ -17,7 +17,7 @@ /** * @inheritdoc */ -class MutableScopeConfig implements MutableScopeConfigInterface +class ApiMutableScopeConfig implements MutableScopeConfigInterface { /** * @var Config @@ -56,7 +56,6 @@ public function setValue( /** * Clean app config cache * - * @param string|null $type * @return void */ public function clean() @@ -89,19 +88,13 @@ private function getTestAppConfig() private function persistConfig($path, $value, $scopeType, $scopeCode): void { $pathParts = explode('/', $path); - $store = ''; - if ($scopeType === \Magento\Store\Model\ScopeInterface::SCOPE_STORE) { - if ($scopeCode !== null) { - $store = ObjectManager::getInstance() + $store = 0; + if ($scopeType === \Magento\Store\Model\ScopeInterface::SCOPE_STORE + && $scopeCode !== null) { + $store = ObjectManager::getInstance() ->get(\Magento\Store\Api\StoreRepositoryInterface::class) ->get($scopeCode) ->getId(); - } else { - $store = ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreManagerInterface::class) - ->getStore() - ->getId(); - } } $configData = [ 'section' => $pathParts[0], diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php index 9ad051b686d47..6400a61b3ef35 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Webapi\Exception as WebapiException; use Magento\Webapi\Model\Soap\Fault; use Magento\TestFramework\Helper\Bootstrap; @@ -102,9 +103,11 @@ abstract class WebapiAbstract extends \PHPUnit\Framework\TestCase /** * Initialize fixture namespaces. + * //phpcs:disable */ public static function setUpBeforeClass() { + //phpcs:enable parent::setUpBeforeClass(); self::_setFixtureNamespace(); } @@ -113,9 +116,11 @@ public static function setUpBeforeClass() * Run garbage collector for cleaning memory * * @return void + * //phpcs:disable */ public static function tearDownAfterClass() { + //phpcs:enable //clear garbage in memory gc_collect_cycles(); @@ -133,8 +138,7 @@ public static function tearDownAfterClass() } /** - * Call safe delete for models which added to delete list - * Restore config values changed during the test + * Call safe delete for models which added to delete list, Restore config values changed during the test * * @return void */ @@ -178,6 +182,8 @@ protected function _webApiCall( /** * Mark test to be executed for SOAP adapter only. + * + * @param ?string $message */ protected function _markTestAsSoapOnly($message = null) { @@ -188,6 +194,8 @@ protected function _markTestAsSoapOnly($message = null) /** * Mark test to be executed for REST adapter only. + * + * @param ?string $message */ protected function _markTestAsRestOnly($message = null) { @@ -203,9 +211,11 @@ protected function _markTestAsRestOnly($message = null) * @param mixed $fixture * @param int $tearDown * @return void + * //phpcs:disable */ public static function setFixture($key, $fixture, $tearDown = self::AUTO_TEAR_DOWN_AFTER_METHOD) { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); if (!isset(self::$_fixtures[$fixturesNamespace])) { self::$_fixtures[$fixturesNamespace] = []; @@ -231,9 +241,11 @@ public static function setFixture($key, $fixture, $tearDown = self::AUTO_TEAR_DO * * @param string $key * @return mixed + * //phpcs:disable */ public static function getFixture($key) { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); if (array_key_exists($key, self::$_fixtures[$fixturesNamespace])) { return self::$_fixtures[$fixturesNamespace][$key]; @@ -247,9 +259,11 @@ public static function getFixture($key) * @param \Magento\Framework\Model\AbstractModel $model * @param bool $secure * @return \Magento\TestFramework\TestCase\WebapiAbstract + * //phpcs:disable */ public static function callModelDelete($model, $secure = false) { + //phpcs:enable if ($model instanceof \Magento\Framework\Model\AbstractModel && $model->getId()) { if ($secure) { self::_enableSecureArea(); @@ -300,9 +314,11 @@ protected function _getWebApiAdapter($webApiAdapterCode) * Set fixtures namespace * * @throws \RuntimeException + * //phpcs:disable */ protected static function _setFixtureNamespace() { + //phpcs:enable if (self::$_fixturesNamespace !== null) { throw new \RuntimeException('Fixture namespace is already set.'); } @@ -311,9 +327,11 @@ protected static function _setFixtureNamespace() /** * Unset fixtures namespace + * //phpcs:disable */ protected static function _unsetFixtureNamespace() { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); unset(self::$_fixtures[$fixturesNamespace]); self::$_fixturesNamespace = null; @@ -324,9 +342,12 @@ protected static function _unsetFixtureNamespace() * * @throws \RuntimeException * @return string + * //phpcs:disable */ protected static function _getFixtureNamespace() { + //phpcs:enable + $fixtureNamespace = self::$_fixturesNamespace; if ($fixtureNamespace === null) { throw new \RuntimeException('Fixture namespace must be set.'); @@ -339,9 +360,12 @@ protected static function _getFixtureNamespace() * * @param bool $flag * @return void + * //phpcs:disable */ protected static function _enableSecureArea($flag = true) { + //phpcs:enable + /** @var $objectManager \Magento\TestFramework\ObjectManager */ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -388,9 +412,11 @@ protected function _assertMessagesEqual($expectedMessages, $receivedMessages) * Delete array of fixtures * * @param array $fixtures + * //phpcs:disable */ protected static function _deleteFixtures($fixtures) { + //phpcs:enable foreach ($fixtures as $fixture) { self::deleteFixture($fixture, true); } @@ -402,9 +428,11 @@ protected static function _deleteFixtures($fixtures) * @param string $key * @param bool $secure * @return void + * //phpcs:disable */ public static function deleteFixture($key, $secure = false) { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); if (array_key_exists($key, self::$_fixtures[$fixturesNamespace])) { self::callModelDelete(self::$_fixtures[$fixturesNamespace][$key], $secure); @@ -456,11 +484,11 @@ protected function _cleanAppConfigCache() /** * Update application config data * - * @param string $path Config path with the form "section/group/node" - * @param string|int|null $value Value of config item - * @param bool $cleanAppCache If TRUE application cache will be refreshed - * @param bool $updateLocalConfig If TRUE local config object will be updated too - * @param bool $restore If TRUE config value will be restored after test run + * @param string $path Config path with the form "section/group/node" + * @param string|int|null $value Value of config item + * @param bool $cleanAppCache If TRUE application cache will be refreshed + * @param bool $updateLocalConfig If TRUE local config object will be updated too + * @param bool $restore If TRUE config value will be restored after test run * @return \Magento\TestFramework\TestCase\WebapiAbstract * @throws \RuntimeException */ @@ -520,6 +548,8 @@ protected function _restoreAppConfig() } /** + * Process rest exception result. + * * @param \Exception $e * @return array * <pre> ex. @@ -666,11 +696,19 @@ protected function _checkWrappedErrors($expectedWrappedErrors, $errorDetails) } /** + * Get actual wrapped errors. + * * @param \stdClass $errorNode * @return array */ private function getActualWrappedErrors(\stdClass $errorNode) { + if (!isset($errorNode->parameters)) { + return [ + 'message' => $errorNode->message, + ]; + } + $actualParameters = []; $parameterNode = $errorNode->parameters->parameter; if (is_array($parameterNode)) { @@ -686,4 +724,42 @@ private function getActualWrappedErrors(\stdClass $errorNode) 'params' => $actualParameters, ]; } + + /** + * Assert webapi errors. + * + * @param array $serviceInfo + * @param array $data + * @param array $expectedErrorData + * @return void + * @throws \Exception + */ + protected function assertWebApiCallErrors(array $serviceInfo, array $data, array $expectedErrorData) + { + try { + $this->_webApiCall($serviceInfo, $data); + $this->fail('Expected throwing exception'); + } catch (\Exception $e) { + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_REST) { + self::assertEquals($expectedErrorData, $this->processRestExceptionResult($e)); + self::assertEquals(WebapiException::HTTP_BAD_REQUEST, $e->getCode()); + } elseif (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $this->assertInstanceOf('SoapFault', $e); + $expectedWrappedErrors = []; + foreach ($expectedErrorData['errors'] as $error) { + // @see \Magento\TestFramework\TestCase\WebapiAbstract::getActualWrappedErrors() + $expectedWrappedError = [ + 'message' => $error['message'], + ]; + if (isset($error['parameters'])) { + $expectedWrappedError['params'] = $error['parameters']; + } + $expectedWrappedErrors[] = $expectedWrappedError; + } + $this->checkSoapFault($e, $expectedErrorData['message'], 'env:Sender', [], $expectedWrappedErrors); + } else { + throw $e; + } + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/BulkStatusInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/BulkStatusInterfaceTest.php new file mode 100644 index 0000000000000..46ffb39878c0d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/BulkStatusInterfaceTest.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Api; + +use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Framework\Bulk\OperationInterface; + +class BulkStatusInterfaceTest extends WebapiAbstract +{ + const RESOURCE_PATH = '/V1/bulk/'; + const SERVICE_NAME = 'asynchronousOperationsBulkStatusV1'; + const GET_COUNT_OPERATION_NAME = "GetOperationsCountByBulkIdAndStatus"; + const TEST_UUID = "bulk-uuid-searchable-6"; + + /** + * @magentoApiDataFixture Magento/AsynchronousOperations/_files/operation_searchable.php + */ + public function testGetListByBulkStartTime() + { + $resourcePath = self::RESOURCE_PATH + . self::TEST_UUID + . "/operation-status/" + . OperationInterface::STATUS_TYPE_OPEN; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . self::GET_COUNT_OPERATION_NAME + ], + ]; + $qty = $this->_webApiCall( + $serviceInfo, + ['bulkUuid' => self::TEST_UUID, 'status' => OperationInterface::STATUS_TYPE_OPEN] + ); + $this->assertEquals(2, $qty); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/OperationRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/OperationRepositoryInterfaceTest.php index 8eab6c9fd8676..81ed561a9803e 100644 --- a/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/OperationRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/AsynchronousOperations/Api/OperationRepositoryInterfaceTest.php @@ -57,8 +57,8 @@ public function testGetListByBulkStartTime() $this->assertArrayHasKey('items', $response); $this->assertEquals($searchCriteria['searchCriteria'], $response['search_criteria']); - $this->assertEquals(3, $response['total_count']); - $this->assertEquals(3, count($response['items'])); + $this->assertEquals(5, $response['total_count']); + $this->assertEquals(5, count($response['items'])); foreach ($response['items'] as $item) { $this->assertEquals('bulk-uuid-searchable-6', $item['bulk_uuid']); diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php index 0293f44615080..6388684466d10 100644 --- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php @@ -282,7 +282,6 @@ protected function getBundleProductOptions($product) protected function setBundleProductOptions(&$product, $bundleProductOptions) { $product["extension_attributes"]["bundle_product_options"] = $bundleProductOptions; - return; } /** @@ -499,7 +498,8 @@ protected function deleteProductBySku($productSku) protected function saveProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + $count = count($product['custom_attributes']); + for ($i=0; $i < $count; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php index 332e509d550ac..d614f6e913dc5 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -6,11 +6,21 @@ */ namespace Magento\Catalog\Api; +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\Rules; +use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\RulesFactory; +/** + * Test repository web API. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CategoryRepositoryTest extends WebapiAbstract { const RESOURCE_PATH = '/V1/categories'; @@ -18,6 +28,33 @@ class CategoryRepositoryTest extends WebapiAbstract private $modelId = 333; + /** + * @var RoleFactory + */ + private $roleFactory; + + /** + * @var RulesFactory + */ + private $rulesFactory; + + /** + * @var AdminTokenServiceInterface + */ + private $adminTokens; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->roleFactory = Bootstrap::getObjectManager()->get(RoleFactory::class); + $this->rulesFactory = Bootstrap::getObjectManager()->get(RulesFactory::class); + $this->adminTokens = Bootstrap::getObjectManager()->get(AdminTokenServiceInterface::class); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/category_backend.php */ @@ -57,8 +94,10 @@ public function testInfoNoSuchEntityException() } /** + * Load category data. + * * @param int $id - * @return string + * @return array */ protected function getInfoCategory($id) { @@ -209,10 +248,11 @@ protected function getSimpleCategoryData($categoryData = []) /** * Create category process * - * @param $category - * @return int + * @param array $category + * @param string|null $token + * @return array */ - protected function createCategory($category) + protected function createCategory(array $category, ?string $token = null) { $serviceInfo = [ 'rest' => [ @@ -225,6 +265,9 @@ protected function createCategory($category) 'operation' => self::SERVICE_NAME . 'Save', ], ]; + if ($token) { + $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; + } $requestData = ['category' => $category]; return $this->_webApiCall($serviceInfo, $requestData); } @@ -251,7 +294,15 @@ protected function deleteCategory($id) return $this->_webApiCall($serviceInfo, ['categoryId' => $id]); } - protected function updateCategory($id, $data) + /** + * Update given category via web API. + * + * @param int $id + * @param array $data + * @param string|null $token + * @return array + */ + protected function updateCategory($id, $data, ?string $token = null) { $serviceInfo = [ @@ -265,6 +316,9 @@ protected function updateCategory($id, $data) 'operation' => self::SERVICE_NAME . 'Save', ], ]; + if ($token) { + $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; + } if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { $data['id'] = $id; @@ -272,7 +326,125 @@ protected function updateCategory($id, $data) } else { $data['id'] = $id; return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); - return $this->_webApiCall($serviceInfo, ['category' => $data]); } } + + /** + * Update admin role resources list. + * + * @param string $roleName + * @param string[] $resources + * @return void + */ + private function updateRoleResources(string $roleName, array $resources): void + { + /** @var Role $role */ + $role = $this->roleFactory->create(); + $role->load($roleName, 'role_name'); + /** @var Rules $rules */ + $rules = $this->rulesFactory->create(); + $rules->setRoleId($role->getId()); + $rules->setResources($resources); + $rules->saveRel(); + } + + /** + * Extract error returned by the server. + * + * @param \Throwable $exception + * @return string + */ + private function extractCallExceptionMessage(\Throwable $exception): string + { + if ($restResponse = json_decode($exception->getMessage(), true)) { + //REST + return $restResponse['message']; + } else { + //SOAP + return $exception->getMessage(); + } + } + + /** + * Test design settings authorization + * + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + * @throws \Throwable + * @return void + */ + public function testSaveDesign(): void + { + //Updating our admin user's role to allow saving categories but not their design settings. + $roleName = 'test_custom_role'; + $this->updateRoleResources($roleName, ['Magento_Catalog::categories']); + //Using the admin user with custom role. + $token = $this->adminTokens->createAdminAccessToken( + 'customRoleUser', + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + + $categoryData = $this->getSimpleCategoryData(); + $categoryData['custom_attributes'][] = ['attribute_code' => 'custom_layout_update_file', 'value' => 'test']; + + //Creating new category with design settings. + $exceptionMessage = null; + try { + $this->createCategory($categoryData, $token); + } catch (\Throwable $exception) { + $exceptionMessage = $this->extractCallExceptionMessage($exception); + } + //We don't have the permissions. + $this->assertEquals('Not allowed to edit the category\'s design attributes', $exceptionMessage); + + //Updating the user role to allow access to design properties. + $this->updateRoleResources($roleName, ['Magento_Catalog::categories', 'Magento_Catalog::edit_category_design']); + //Making the same request with design settings. + $categoryData = $this->getSimpleCategoryData(); + foreach ($categoryData['custom_attributes'] as &$attribute) { + if ($attribute['attribute_code'] === 'custom_design') { + $attribute['value'] = 'test'; + break; + } + } + $result = $this->createCategory($categoryData, $token); + $this->assertArrayHasKey('id', $result); + //Category must be saved. + $categorySaved = $this->getInfoCategory($result['id']); + $savedCustomDesign = null; + foreach ($categorySaved['custom_attributes'] as $customAttribute) { + if ($customAttribute['attribute_code'] === 'custom_design') { + $savedCustomDesign = $customAttribute['value']; + break; + } + } + $this->assertEquals('test', $savedCustomDesign); + $categoryData = $categorySaved; + + //Updating our role to remove design properties access. + $this->updateRoleResources($roleName, ['Magento_Catalog::categories']); + //Updating the category but with the same design properties values. + //Omitting existing design attribute and keeping it's existing value + $attributes = $categoryData['custom_attributes']; + foreach ($attributes as $index => $attrData) { + if ($attrData['attribute_code'] === 'custom_design') { + unset($categoryData['custom_attributes'][$index]); + break; + } + } + unset($attributes, $index, $attrData); + $result = $this->updateCategory($categoryData['id'], $categoryData, $token); + //We haven't changed the design so operation is successful. + $this->assertArrayHasKey('id', $result); + + //Changing a design property. + $categoryData['custom_attributes'][] = ['attribute_code' => 'custom_design', 'value' => 'test2']; + $exceptionMessage = null; + try { + $this->updateCategory($categoryData['id'], $categoryData, $token); + } catch (\Throwable $exception) { + $exceptionMessage = $this->extractCallExceptionMessage($exception); + } + //We don't have permissions to do that. + $this->assertEquals('Not allowed to edit the category\'s design attributes', $exceptionMessage); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php index 9a6520a3ab458..a192936aeaccf 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php @@ -4,16 +4,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Api; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Framework\ObjectManagerInterface; /** * Class ProductAttributeMediaGalleryManagementInterfaceTest */ -class ProductAttributeMediaGalleryManagementInterfaceTest extends \Magento\TestFramework\TestCase\WebapiAbstract +class ProductAttributeMediaGalleryManagementInterfaceTest extends WebapiAbstract { /** * Default create service request information (product with SKU 'simple' is used) @@ -41,12 +48,22 @@ class ProductAttributeMediaGalleryManagementInterfaceTest extends \Magento\TestF */ protected $testImagePath; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ protected function setUp() { + $this->objectManager = Bootstrap::getObjectManager(); + $this->createServiceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/simple/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -58,7 +75,7 @@ protected function setUp() $this->updateServiceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/simple/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -66,9 +83,10 @@ protected function setUp() 'operation' => 'catalogProductAttributeMediaGalleryManagementV1Update', ], ]; + $this->deleteServiceInfo = [ 'rest' => [ - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -76,6 +94,7 @@ protected function setUp() 'operation' => 'catalogProductAttributeMediaGalleryManagementV1Remove', ], ]; + $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; } @@ -86,8 +105,7 @@ protected function setUp() */ protected function getTargetSimpleProduct() { - $objectManager = Bootstrap::getObjectManager(); - return $objectManager->get(\Magento\Catalog\Model\ProductFactory::class)->create()->load(1); + return $this->objectManager->get(ProductFactory::class)->create()->load(1); } /** @@ -101,17 +119,20 @@ protected function getTargetGalleryEntryId() { $mediaGallery = $this->getTargetSimpleProduct()->getData('media_gallery'); $image = array_shift($mediaGallery['images']); + return (int)$image['value_id']; } /** + * Test create() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testCreate() { $requestData = [ 'id' => null, - 'media_type' => \Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter::MEDIA_TYPE_CODE, + 'media_type' => ImageEntryConverter::MEDIA_TYPE_CODE, 'label' => 'Image Text', 'position' => 1, 'types' => ['image'], @@ -138,13 +159,15 @@ public function testCreate() } /** + * Test create() method without file + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testCreateWithoutFileExtension() { $requestData = [ 'id' => null, - 'media_type' => \Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter::MEDIA_TYPE_CODE, + 'media_type' => ImageEntryConverter::MEDIA_TYPE_CODE, 'label' => 'Image Text', 'position' => 1, 'types' => ['image'], @@ -171,13 +194,15 @@ public function testCreateWithoutFileExtension() } /** + * Test create() method with not default store id + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testCreateWithNotDefaultStoreId() { $requestData = [ 'id' => null, - 'media_type' => \Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter::MEDIA_TYPE_CODE, + 'media_type' => ImageEntryConverter::MEDIA_TYPE_CODE, 'label' => 'Image Text', 'position' => 1, 'types' => ['image'], @@ -215,10 +240,16 @@ public function testCreateWithNotDefaultStoreId() } /** + * Test update() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testUpdate() { + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple'); + $imageId = (int)$product->getMediaGalleryImages()->getFirstItem()->getValueId(); $requestData = [ 'sku' => 'simple', 'entry' => [ @@ -235,28 +266,64 @@ public function testUpdate() . '/' . $this->getTargetGalleryEntryId(); $this->assertTrue($this->_webApiCall($this->updateServiceInfo, $requestData, null, 'all')); + $updatedImage = $this->assertMediaGalleryData($imageId, '/m/a/magento_image.jpg', 'Updated Image Text'); + $this->assertEquals(10, $updatedImage['position_default']); + $this->assertEquals(1, $updatedImage['disabled_default']); + } - $targetProduct = $this->getTargetSimpleProduct(); - $this->assertEquals('/m/a/magento_image.jpg', $targetProduct->getData('thumbnail')); - $this->assertEquals('no_selection', $targetProduct->getData('image')); - $this->assertEquals('no_selection', $targetProduct->getData('small_image')); - $mediaGallery = $targetProduct->getData('media_gallery'); - $this->assertCount(1, $mediaGallery['images']); - $updatedImage = array_shift($mediaGallery['images']); - $this->assertEquals('Updated Image Text', $updatedImage['label']); - $this->assertEquals('/m/a/magento_image.jpg', $updatedImage['file']); - $this->assertEquals(10, $updatedImage['position']); - $this->assertEquals(1, $updatedImage['disabled']); - $this->assertEquals('Updated Image Text', $updatedImage['label_default']); + /** + * Update media gallery entity with new image. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + * @return void + */ + public function testUpdateWithNewImage(): void + { + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple'); + $imageId = (int)$product->getMediaGalleryImages()->getFirstItem()->getValueId(); + + $requestData = [ + 'sku' => 'simple', + 'entry' => [ + 'id' => $this->getTargetGalleryEntryId(), + 'label' => 'Updated Image Text', + 'position' => 10, + 'types' => ['thumbnail'], + 'disabled' => true, + 'media_type' => 'image', + 'content' => [ + 'base64_encoded_data' => 'iVBORw0KGgoAAAANSUhEUgAAAP8AAADGCAMAAAAqo6adAAAAA1BMVEUAAP79f' + . '+LBAAAASElEQVR4nO3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + . 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA+BsYAAAF7hZJ0AAAAAElFTkSuQmCC', + 'type' => 'image/png', + 'name' => 'testname_updated.png', + ], + ], + ]; + + $this->updateServiceInfo['rest']['resourcePath'] = $this->updateServiceInfo['rest']['resourcePath'] + . '/' . $this->getTargetGalleryEntryId(); + + $this->assertTrue($this->_webApiCall($this->updateServiceInfo, $requestData, null, 'all')); + $updatedImage = $this->assertMediaGalleryData($imageId, '/t/e/testname_updated.png', 'Updated Image Text'); $this->assertEquals(10, $updatedImage['position_default']); $this->assertEquals(1, $updatedImage['disabled_default']); } /** + * Test update() method with not default store id + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testUpdateWithNotDefaultStoreId() { + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple'); + $imageId = (int)$product->getMediaGalleryImages()->getFirstItem()->getValueId(); + $requestData = [ 'sku' => 'simple', 'entry' => [ @@ -273,25 +340,42 @@ public function testUpdateWithNotDefaultStoreId() . '/' . $this->getTargetGalleryEntryId(); $this->assertTrue($this->_webApiCall($this->updateServiceInfo, $requestData, null, 'default')); + $updatedImage = $this->assertMediaGalleryData($imageId, '/m/a/magento_image.jpg', 'Image Alt Text'); + $this->assertEquals(1, $updatedImage['position_default']); + $this->assertEquals(0, $updatedImage['disabled_default']); + } + /** + * Check that Media Gallery data is correct. + * + * @param int $imageId + * @param string $file + * @param string $label + * @return array + */ + private function assertMediaGalleryData(int $imageId, string $file, string $label): array + { $targetProduct = $this->getTargetSimpleProduct(); - $this->assertEquals('/m/a/magento_image.jpg', $targetProduct->getData('thumbnail')); + $this->assertEquals($file, $targetProduct->getData('thumbnail')); + $this->assertEquals('no_selection', $targetProduct->getData('image')); + $this->assertEquals('no_selection', $targetProduct->getData('small_image')); $mediaGallery = $targetProduct->getData('media_gallery'); $this->assertCount(1, $mediaGallery['images']); $updatedImage = array_shift($mediaGallery['images']); - // Not default store view values were updated + $this->assertEquals($imageId, $updatedImage['value_id']); $this->assertEquals('Updated Image Text', $updatedImage['label']); - $this->assertEquals('/m/a/magento_image.jpg', $updatedImage['file']); + $this->assertEquals($file, $updatedImage['file']); $this->assertEquals(10, $updatedImage['position']); $this->assertEquals(1, $updatedImage['disabled']); - // Default store view values were not updated - $this->assertEquals('Image Alt Text', $updatedImage['label_default']); - $this->assertEquals(1, $updatedImage['position_default']); - $this->assertEquals(0, $updatedImage['disabled_default']); + $this->assertEquals($label, $updatedImage['label_default']); + + return $updatedImage; } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + * Test delete() method + * + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image_without_types.php */ public function testDelete() { @@ -309,6 +393,8 @@ public function testDelete() } /** + * Test create() method if provided content is not base64 encoded + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage The image content must be valid base64 encoded data. @@ -334,6 +420,8 @@ public function testCreateThrowsExceptionIfProvidedContentIsNotBase64Encoded() } /** + * Test create() method if provided content is not an image + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage The image content must be valid base64 encoded data. @@ -359,6 +447,8 @@ public function testCreateThrowsExceptionIfProvidedContentIsNotAnImage() } /** + * Test create() method if provided image has wrong MIME type + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage The image MIME type is not valid or not supported. @@ -384,6 +474,8 @@ public function testCreateThrowsExceptionIfProvidedImageHasWrongMimeType() } /** + * Test create method if target product does not exist + * * @expectedException \Exception * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. */ @@ -409,6 +501,8 @@ public function testCreateThrowsExceptionIfTargetProductDoesNotExist() } /** + * Test create() method if provided image name contains forbidden characters + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage Provided image name contains forbidden characters. @@ -433,6 +527,8 @@ public function testCreateThrowsExceptionIfProvidedImageNameContainsForbiddenCha } /** + * Test update() method if target product does not exist + * * @expectedException \Exception * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. */ @@ -456,6 +552,8 @@ public function testUpdateThrowsExceptionIfTargetProductDoesNotExist() } /** + * Test update() method if there is no image with given id + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php * @expectedException \Exception * @expectedExceptionMessage No image with the provided ID was found. Verify the ID and try again. @@ -481,6 +579,8 @@ public function testUpdateThrowsExceptionIfThereIsNoImageWithGivenId() } /** + * Test delete() method if target product does not exist + * * @expectedException \Exception * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. */ @@ -496,6 +596,8 @@ public function testDeleteThrowsExceptionIfTargetProductDoesNotExist() } /** + * Test delete() method if there is no image with given id + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php * @expectedException \Exception * @expectedExceptionMessage No image with the provided ID was found. Verify the ID and try again. @@ -512,15 +614,16 @@ public function testDeleteThrowsExceptionIfThereIsNoImageWithGivenId() } /** + * Test get() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testGet() { $productSku = 'simple'; - $objectManager = \Magento\TestFramework\ObjectManager::getInstance(); - /** @var \Magento\Catalog\Model\ProductRepository $repository */ - $repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + /** @var ProductRepository $repository */ + $repository = $this->objectManager->create(ProductRepository::class); $product = $repository->get($productSku); $image = current($product->getMediaGallery('images')); $imageId = $image['value_id']; @@ -537,7 +640,7 @@ public function testGet() $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/' . $productSku . '/media/' . $imageId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -560,6 +663,8 @@ public function testGet() } /** + * Test getList() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testGetList() @@ -568,7 +673,7 @@ public function testGetList() $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/' . urlencode($productSku) . '/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -591,13 +696,16 @@ public function testGetList() $this->assertContains('thumbnail', $imageTypes); } + /** + * Test getList() method for absent sku + */ public function testGetListForAbsentSku() { $productSku = 'absent_sku_' . time(); $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/' . urlencode($productSku) . '/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -622,6 +730,8 @@ public function testGetListForAbsentSku() } /** + * Test addProductVideo() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testAddProductVideo() diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeRepositoryTest.php index 386bd9fc9aeeb..42aa92652a5f1 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeRepositoryTest.php @@ -4,12 +4,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Catalog\Api; use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; -use Magento\TestFramework\Helper\Bootstrap; +/** + * API tests for \Magento\Catalog\Model\Product\Attribute\Repository. + */ class ProductAttributeRepositoryTest extends \Magento\TestFramework\TestCase\WebapiAbstract { const SERVICE_NAME = 'catalogProductAttributeRepositoryV1'; @@ -130,6 +131,7 @@ public function testCreateWithExceptionIfAttributeAlreadyExists() try { $this->createAttribute($attributeCode); $this->fail("Expected exception"); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (\SoapFault $e) { //Expects soap exception } catch (\Exception $e) { @@ -320,6 +322,20 @@ public function testDeleteById() $this->assertTrue($this->deleteAttribute($attributeCode)); } + /** + * Trying to delete system attribute. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_system_attribute.php + * @expectedException \Exception + * @expectedExceptionMessage The system attribute can't be deleted. + * @return void + */ + public function testDeleteSystemAttributeById(): void + { + $attributeCode = 'test_attribute_code_333'; + $this->deleteAttribute($attributeCode); + } + /** * @return void */ diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductLinkManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductLinkManagementInterfaceTest.php index 011a1e40407ac..1ac61bc860759 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductLinkManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductLinkManagementInterfaceTest.php @@ -81,6 +81,7 @@ protected function assertLinkedProducts($productSku, $linkType) $actual = $this->_webApiCall($serviceInfo, ['sku' => $productSku, 'type' => $linkType]); + $this->assertArrayHasKey(0, $actual); $this->assertEquals('simple', $actual[0]['linked_product_type']); $this->assertEquals('simple', $actual[0]['linked_product_sku']); $this->assertEquals(1, $actual[0]['position']); @@ -122,9 +123,13 @@ public function testAssign() $this->_webApiCall($serviceInfo, $arguments); $actual = $this->getLinkedProducts($productSku, 'related'); - array_walk($actual, function (&$item) { - $item = $item->__toArray(); - }); + array_walk( + $actual, + function (&$item) { + /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $item */ + $item = $item->__toArray(); + } + ); $this->assertEquals([$linkData], $actual); } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 3e935e1d7ae9b..76107ebc6a13a 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -7,9 +7,15 @@ namespace Magento\Catalog\Api; +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\RulesFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\Downloadable\Api\DomainManagerInterface; use Magento\Downloadable\Model\Link; +use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\Store\Model\Store; use Magento\Store\Model\Website; use Magento\Store\Model\WebsiteRepository; @@ -24,6 +30,8 @@ use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; /** + * Test for \Magento\Catalog\Api\ProductRepositoryInterface + * * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -56,6 +64,51 @@ class ProductRepositoryInterfaceTest extends WebapiAbstract ]; /** + * @var RoleFactory + */ + private $roleFactory; + + /** + * @var RulesFactory + */ + private $rulesFactory; + + /** + * @var AdminTokenServiceInterface + */ + private $adminTokens; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->roleFactory = Bootstrap::getObjectManager()->get(RoleFactory::class); + $this->rulesFactory = Bootstrap::getObjectManager()->get(RulesFactory::class); + $this->adminTokens = Bootstrap::getObjectManager()->get(AdminTokenServiceInterface::class); + /** @var DomainManagerInterface $domainManager */ + $domainManager = Bootstrap::getObjectManager()->get(DomainManagerInterface::class); + $domainManager->addDomains(['example.com']); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + parent::tearDown(); + + $objectManager = Bootstrap::getObjectManager(); + /** @var DomainManagerInterface $domainManager */ + $domainManager = $objectManager->get(DomainManagerInterface::class); + $domainManager->removeDomains(['example.com']); + } + + /** + * Test get() method + * * @magentoApiDataFixture Magento/Catalog/_files/products_related.php */ public function testGet() @@ -69,6 +122,8 @@ public function testGet() } /** + * Get product + * * @param string $sku * @param string|null $storeCode * @return array|bool|float|int|string @@ -86,11 +141,14 @@ protected function getProduct($sku, $storeCode = null) 'operation' => self::SERVICE_NAME . 'Get', ], ]; - $response = $this->_webApiCall($serviceInfo, ['sku' => $sku], null, $storeCode); + return $response; } + /** + * Test get() method with invalid sku + */ public function testGetNoSuchEntityException() { $invalidSku = '(nonExistingSku)'; @@ -125,6 +183,8 @@ public function testGetNoSuchEntityException() } /** + * Product creation provider + * * @return array */ public function productCreationProvider() @@ -135,6 +195,7 @@ public function productCreationProvider() $data ); }; + return [ [$productBuilder([ProductInterface::TYPE_ID => 'simple', ProductInterface::SKU => 'psku-test-1'])], [$productBuilder([ProductInterface::TYPE_ID => 'virtual', ProductInterface::SKU => 'psku-test-2'])], @@ -161,6 +222,7 @@ private function loadWebsiteByCode($websiteCode) /** * Test removing association between product and website 1 + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php */ public function testUpdateWithDeleteWebsites() @@ -184,6 +246,7 @@ public function testUpdateWithDeleteWebsites() /** * Test removing all website associations + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php */ public function testDeleteAllWebsiteAssociations() @@ -202,6 +265,8 @@ public function testDeleteAllWebsiteAssociations() } /** + * Test create() method with multiple websites + * * @magentoApiDataFixture Magento/Catalog/_files/second_website.php */ public function testCreateWithMultipleWebsites() @@ -305,6 +370,8 @@ public function testUpdateWithoutWebsiteIds() } /** + * Test create() method + * * @dataProvider productCreationProvider */ public function testCreate($product) @@ -372,6 +439,9 @@ public function testCreateAllStoreCodeForSingleWebsite($fixtureProduct) $this->deleteProduct($fixtureProduct[ProductInterface::SKU]); } + /** + * Test create() method with invalid price format + */ public function testCreateInvalidPriceFormat() { $this->_markTestAsRestOnly("In case of SOAP type casting is handled by PHP SoapServer, no need to test it"); @@ -408,6 +478,9 @@ public function testDeleteAllStoreCode($fixtureProduct) $this->getProduct($sku); } + /** + * Test product links + */ public function testProductLinks() { // Create simple product @@ -441,7 +514,6 @@ public function testProductLinks() ProductInterface::TYPE_ID => 'simple', ProductInterface::PRICE => 100, ProductInterface::STATUS => 1, - ProductInterface::TYPE_ID => 'simple', ProductInterface::ATTRIBUTE_SET_ID => 4, "product_links" => [$productLinkData] ]; @@ -504,6 +576,8 @@ public function testProductLinks() } /** + * Get options data + * * @param string $productSku * @return array */ @@ -543,6 +617,9 @@ protected function getOptionsData($productSku) ]; } + /** + * Test product options + */ public function testProductOptions() { //Create product with options @@ -604,9 +681,13 @@ public function testProductOptions() $this->deleteProduct($productData[ProductInterface::SKU]); } + /** + * Test product with media gallery + */ public function testProductWithMediaGallery() { $testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; + // @codingStandardsIgnoreLine $encodedImage = base64_encode(file_get_contents($testImagePath)); //create a product with media gallery $filename1 = 'tiny1' . time() . '.jpg'; @@ -635,7 +716,7 @@ public function testProductWithMediaGallery() 'position' => 2, 'media_type' => 'image', 'disabled' => false, - 'types' => ['image', 'small_image'], + 'types' => [], 'file' => '/t/i/' . $filename2, ], ]; @@ -648,7 +729,7 @@ public function testProductWithMediaGallery() 'label' => 'tiny1_new_label', 'position' => 1, 'disabled' => false, - 'types' => ['image', 'small_image'], + 'types' => [], 'file' => '/t/i/' . $filename1, ], ]; @@ -662,7 +743,7 @@ public function testProductWithMediaGallery() 'media_type' => 'image', 'position' => 1, 'disabled' => false, - 'types' => ['image', 'small_image'], + 'types' => [], 'file' => '/t/i/' . $filename1, ] ]; @@ -682,6 +763,8 @@ public function testProductWithMediaGallery() } /** + * Test update() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testUpdate() @@ -725,17 +808,18 @@ public function testUpdateWithExtensionAttributes(): void } /** + * Update product + * * @param array $product + * @param string|null $token * @return array|bool|float|int|string */ - protected function updateProduct($product) + protected function updateProduct($product, ?string $token = null) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { - if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' - && !is_array($product['custom_attributes'][$i]['value']) - ) { - $product['custom_attributes'][$i]['value'] = [""]; + foreach ($product['custom_attributes'] as &$attribute) { + if ($attribute['attribute_code'] == 'category_ids' && !is_array($attribute['value'])) { + $attribute['value'] = [""]; } } } @@ -755,12 +839,17 @@ protected function updateProduct($product) 'operation' => self::SERVICE_NAME . 'Save', ], ]; + if ($token) { + $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; + } $requestData = ['product' => $product]; $response = $this->_webApiCall($serviceInfo, $requestData); return $response; } /** + * Test delete() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testDelete() @@ -770,6 +859,8 @@ public function testDelete() } /** + * Test getList() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testGetList() @@ -832,6 +923,8 @@ public function testGetList() } /** + * Test getList() method with additional params + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testGetListWithAdditionalParams() @@ -871,6 +964,8 @@ public function testGetListWithAdditionalParams() } /** + * Test getList() method with filtering by website + * * @magentoApiDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php * @return void */ @@ -958,6 +1053,11 @@ public function testGetListWithFilteringByStore(array $searchCriteria, array $sk } } + /** + * Test getList() method with filtering by store data provider + * + * @return array + */ public function testGetListWithFilteringByStoreDataProvider() { return [ @@ -997,6 +1097,8 @@ public function testGetListWithFilteringByStoreDataProvider() } /** + * Test getList() method with multiple filter groups and sorting and pagination + * * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php */ public function testGetListWithMultipleFilterGroupsAndSortingAndPagination() @@ -1066,6 +1168,8 @@ public function testGetListWithMultipleFilterGroupsAndSortingAndPagination() } /** + * Convert custom attributes to associative array + * * @param $customAttributes * @return array */ @@ -1075,10 +1179,13 @@ protected function convertCustomAttributesToAssociativeArray($customAttributes) foreach ($customAttributes as $customAttribute) { $converted[$customAttribute['attribute_code']] = $customAttribute['value']; } + return $converted; } /** + * Convert associative array to custom attributes + * * @param $data * @return array */ @@ -1088,10 +1195,13 @@ protected function convertAssociativeArrayToCustomAttributes($data) foreach ($data as $attributeCode => $attributeValue) { $customAttributes[] = ['attribute_code' => $attributeCode, 'value' => $attributeValue]; } + return $customAttributes; } /** + * Test eav attributes + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testEavAttributes() @@ -1135,7 +1245,6 @@ protected function getSimpleProductData($productData = []) ProductInterface::TYPE_ID => 'simple', ProductInterface::PRICE => 3.62, ProductInterface::STATUS => 1, - ProductInterface::TYPE_ID => 'simple', ProductInterface::ATTRIBUTE_SET_ID => 4, 'custom_attributes' => [ ['attribute_code' => 'cost', 'value' => ''], @@ -1145,18 +1254,21 @@ protected function getSimpleProductData($productData = []) } /** + * Save Product + * * @param $product * @param string|null $storeCode + * @param string|null $token * @return mixed */ - protected function saveProduct($product, $storeCode = null) + protected function saveProduct($product, $storeCode = null, ?string $token = null) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { - if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' - && !is_array($product['custom_attributes'][$i]['value']) + foreach ($product['custom_attributes'] as &$attribute) { + if ($attribute['attribute_code'] == 'category_ids' + && !is_array($attribute['value']) ) { - $product['custom_attributes'][$i]['value'] = [""]; + $attribute['value'] = [""]; } } } @@ -1171,7 +1283,11 @@ protected function saveProduct($product, $storeCode = null) 'operation' => self::SERVICE_NAME . 'Save', ], ]; + if ($token) { + $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; + } $requestData = ['product' => $product]; + return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode); } @@ -1199,6 +1315,9 @@ protected function deleteProduct($sku) $this->_webApiCall($serviceInfo, ['sku' => $sku]) : $this->_webApiCall($serviceInfo); } + /** + * Test tier prices + */ public function testTierPrices() { // create a product with tier prices @@ -1283,6 +1402,8 @@ public function testTierPrices() } /** + * Get stock item data + * * @return array */ private function getStockItemData() @@ -1315,6 +1436,8 @@ private function getStockItemData() } /** + * Test product category links + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testProductCategoryLinks() @@ -1337,6 +1460,8 @@ public function testProductCategoryLinks() } /** + * Test update product category without categories + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testUpdateProductCategoryLinksNullOrNotExists() @@ -1358,6 +1483,8 @@ public function testUpdateProductCategoryLinksNullOrNotExists() } /** + * Test update product category links position + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testUpdateProductCategoryLinksPosistion() @@ -1375,6 +1502,8 @@ public function testUpdateProductCategoryLinksPosistion() } /** + * Test update product category links unassing + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testUpdateProductCategoryLinksUnassign() @@ -1387,6 +1516,8 @@ public function testUpdateProductCategoryLinksUnassign() } /** + * Get media gallery data + * * @param $filename1 * @param $encodedImage * @param $filename2 @@ -1412,7 +1543,7 @@ private function getMediaGalleryData($filename1, $encodedImage, $filename2) 'media_type' => 'image', 'disabled' => false, 'label' => 'tiny2', - 'types' => ['image', 'small_image'], + 'types' => [], 'content' => [ 'type' => 'image/jpeg', 'name' => $filename2, @@ -1422,6 +1553,9 @@ private function getMediaGalleryData($filename1, $encodedImage, $filename2) ]; } + /** + * Test special price + */ public function testSpecialPrice() { $productData = $this->getSimpleProductData(); @@ -1471,6 +1605,9 @@ public function testResetSpecialPrice() $this->assertFalse(array_key_exists(self::KEY_SPECIAL_PRICE, $customAttributes)); } + /** + * Test update status + */ public function testUpdateStatus() { // Create simple product @@ -1543,6 +1680,8 @@ public function testUpdateMultiselectAttributes() } /** + * Get attribute options + * * @param string $attributeCode * @return array|bool|float|int|string */ @@ -1564,6 +1703,8 @@ private function getAttributeOptions($attributeCode) } /** + * Assert multiselect value + * * @param string $productSku * @param string $multiselectAttributeCode * @param string $expectedMultiselectValue @@ -1582,4 +1723,106 @@ private function assertMultiselectValue($productSku, $multiselectAttributeCode, } $this->assertEquals($expectedMultiselectValue, $multiselectValue); } + + /** + * Test design settings authorization + * + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + * @throws \Throwable + * @return void + */ + public function testSaveDesign(): void + { + //Updating our admin user's role to allow saving products but not their design settings. + /** @var Role $role */ + $role = $this->roleFactory->create(); + $role->load('test_custom_role', 'role_name'); + /** @var Rules $rules */ + $rules = $this->rulesFactory->create(); + $rules->setRoleId($role->getId()); + $rules->setResources(['Magento_Catalog::products']); + $rules->saveRel(); + //Using the admin user with custom role. + $token = $this->adminTokens->createAdminAccessToken( + 'customRoleUser', + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + + $productData = $this->getSimpleProductData(); + $productData['custom_attributes'][] = ['attribute_code' => 'custom_design', 'value' => '1']; + + //Creating new product with design settings. + $exceptionMessage = null; + try { + $this->saveProduct($productData, null, $token); + } catch (\Throwable $exception) { + if ($restResponse = json_decode($exception->getMessage(), true)) { + //REST + $exceptionMessage = $restResponse['message']; + } else { + //SOAP + $exceptionMessage = $exception->getMessage(); + } + } + //We don't have the permissions. + $this->assertEquals('Not allowed to edit the product\'s design attributes', $exceptionMessage); + + //Updating the user role to allow access to design properties. + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules->setRoleId($role->getId()); + $rules->setResources(['Magento_Catalog::products', 'Magento_Catalog::edit_product_design']); + $rules->saveRel(); + //Making the same request with design settings. + $result = $this->saveProduct($productData, null, $token); + $this->assertArrayHasKey('id', $result); + //Product must be saved. + $productSaved = $this->getProduct($productData[ProductInterface::SKU]); + $savedCustomDesign = null; + foreach ($productSaved['custom_attributes'] as $customAttribute) { + if ($customAttribute['attribute_code'] === 'custom_design') { + $savedCustomDesign = $customAttribute['value']; + break; + } + } + $this->assertEquals('1', $savedCustomDesign); + $productData = $productSaved; + + //Updating our role to remove design properties access. + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules->setRoleId($role->getId()); + $rules->setResources(['Magento_Catalog::products']); + $rules->saveRel(); + //Updating the product but with the same design properties values. + //Removing the design attribute and keeping existing value. + $attributes = $productData['custom_attributes']; + foreach ($attributes as $i => $attribute) { + if ($attribute['attribute_code'] === 'custom_design') { + unset($productData['custom_attributes'][$i]); + break; + } + } + unset($attributes, $attribute, $i); + $result = $this->updateProduct($productData, $token); + //We haven't changed the design so operation is successful. + $this->assertArrayHasKey('id', $result); + + //Changing a design property. + $productData['custom_attributes'][] = ['attribute_code' => 'custom_design', 'value' => '2']; + $exceptionMessage = null; + try { + $this->updateProduct($productData, $token); + } catch (\Throwable $exception) { + if ($restResponse = json_decode($exception->getMessage(), true)) { + //REST + $exceptionMessage = $restResponse['message']; + } else { + //SOAP + $exceptionMessage = $exception->getMessage(); + } + } + //We don't have permissions to do that. + $this->assertEquals('Not allowed to edit the product\'s design attributes', $exceptionMessage); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php index 844230f4e3337..65e1e65e36beb 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php @@ -17,6 +17,7 @@ class ProductRepositoryMultiCurrencyTest extends WebapiAbstract const WEBSITES_RESOURCE_PATH = '/V1/store/websites'; /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Store/_files/second_website_with_second_currency.php * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/ProductRepositoryInterfaceTest.php index 5422362afd73c..f1d6949408f5b 100644 --- a/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/ProductRepositoryInterfaceTest.php @@ -306,7 +306,8 @@ protected function getProduct($sku) protected function saveProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + $count = count($product['custom_attributes']); + for ($i=0; $i < $count; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { @@ -339,7 +340,8 @@ protected function saveProduct($product) protected function updateProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + $count = count($product['custom_attributes']); + for ($i=0; $i < $count; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index c40d1918cca67..015eb067e4c8e 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -5,16 +5,23 @@ */ namespace Magento\Cms\Api; +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\RulesFactory; use Magento\Cms\Api\Data\PageInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\SortOrderBuilder; +use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; /** * Tests for cms page service. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PageRepositoryTest extends WebapiAbstract { @@ -47,6 +54,21 @@ class PageRepositoryTest extends WebapiAbstract */ protected $currentPage; + /** + * @var RoleFactory + */ + private $roleFactory; + + /** + * @var RulesFactory + */ + private $rulesFactory; + + /** + * @var AdminTokenServiceInterface + */ + private $adminTokens; + /** * Execute per test initialization. */ @@ -57,6 +79,9 @@ public function setUp() $this->dataObjectHelper = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\DataObjectHelper::class); $this->dataObjectProcessor = Bootstrap::getObjectManager() ->create(\Magento\Framework\Reflection\DataObjectProcessor::class); + $this->roleFactory = Bootstrap::getObjectManager()->get(RoleFactory::class); + $this->rulesFactory = Bootstrap::getObjectManager()->get(RulesFactory::class); + $this->adminTokens = Bootstrap::getObjectManager()->get(AdminTokenServiceInterface::class); } /** @@ -379,4 +404,109 @@ private function deletePageByIdentifier($pageId) $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $pageId]); } + + /** + * Check that extra authorization is required for the design properties. + * + * @magentoApiDataFixture Magento/User/_files/user_with_custom_role.php + * @throws \Throwable + * @return void + */ + public function testSaveDesign(): void + { + //Updating our admin user's role to allow saving pages but not their design settings. + /** @var Role $role */ + $role = $this->roleFactory->create(); + $role->load('test_custom_role', 'role_name'); + /** @var Rules $rules */ + $rules = $this->rulesFactory->create(); + $rules->setRoleId($role->getId()); + $rules->setResources(['Magento_Cms::page']); + $rules->saveRel(); + //Using the admin user with custom role. + $token = $this->adminTokens->createAdminAccessToken( + 'customRoleUser', + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ); + + $id = 'test-cms-page'; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'token' => $token, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + 'token' => $token + ], + ]; + $requestData = [ + 'page' => [ + PageInterface::IDENTIFIER => $id, + PageInterface::TITLE => 'Page title', + PageInterface::CUSTOM_THEME => 1 + ], + ]; + + //Creating new page with design settings. + $exceptionMessage = null; + try { + $this->_webApiCall($serviceInfo, $requestData); + } catch (\Throwable $exception) { + if ($restResponse = json_decode($exception->getMessage(), true)) { + //REST + $exceptionMessage = $restResponse['message']; + } else { + //SOAP + $exceptionMessage = $exception->getMessage(); + } + } + //We don't have the permissions. + $this->assertEquals('You are not allowed to change CMS pages design settings', $exceptionMessage); + + //Updating the user role to allow access to design properties. + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules->setRoleId($role->getId()); + $rules->setResources(['Magento_Cms::page', 'Magento_Cms::save_design']); + $rules->saveRel(); + //Making the same request with design settings. + $result = $this->_webApiCall($serviceInfo, $requestData); + $this->assertArrayHasKey('id', $result); + //Page must be saved. + $this->currentPage = $this->pageRepository->getById($result['id']); + $this->assertEquals($id, $this->currentPage->getIdentifier()); + $this->assertEquals(1, $this->currentPage->getCustomTheme()); + $requestData['page']['id'] = $this->currentPage->getId(); + + //Updating our role to remove design properties access. + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules->setRoleId($role->getId()); + $rules->setResources(['Magento_Cms::page']); + $rules->saveRel(); + //Updating the page but with the same design properties values. + $result = $this->_webApiCall($serviceInfo, $requestData); + //We haven't changed the design so operation is successful. + $this->assertArrayHasKey('id', $result); + //Changing a design property. + $requestData['page'][PageInterface::CUSTOM_THEME] = 2; + $exceptionMessage = null; + try { + $this->_webApiCall($serviceInfo, $requestData); + } catch (\Throwable $exception) { + if ($restResponse = json_decode($exception->getMessage(), true)) { + //REST + $exceptionMessage = $restResponse['message']; + } else { + //SOAP + $exceptionMessage = $exception->getMessage(); + } + } + //We don't have permissions to do that. + $this->assertEquals('You are not allowed to change CMS pages design settings', $exceptionMessage); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php index dc32bb2fc129a..1dc7ca1ad44a6 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php @@ -462,7 +462,8 @@ protected function deleteProductBySku($productSku) protected function saveProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + $count = count($product['custom_attributes']); + for ($i=0; $i < $count; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php index 31894c1332ad5..88bb3a8d59afd 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php @@ -228,14 +228,21 @@ public function testGetCustomerActivateCustomer() 'token' => $this->token ] ]; - $requestData = ['confirmationKey' => $this->customerData[CustomerInterface::CONFIRMATION]]; + + $requestData = ['confirmationKey' => CustomerHelper::CONFIRMATION]; if (TESTS_WEB_API_ADAPTER === 'soap') { $requestData['customerId'] = 0; } - $customerResponseData = $this->_webApiCall($serviceInfo, $requestData); - $this->assertEquals($this->customerData[CustomerInterface::ID], $customerResponseData[CustomerInterface::ID]); - // Confirmation key is removed after confirmation - $this->assertFalse(isset($customerResponseData[CustomerInterface::CONFIRMATION])); + + try { + $customerResponseData = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals( + $this->customerData[CustomerInterface::ID], + $customerResponseData[CustomerInterface::ID] + ); + } catch (\Exception $e) { + $this->fail('Customer is not activated.'); + } } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php index b2276d79f5ecf..a93bbcbdf04b2 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php @@ -249,7 +249,15 @@ public function testCreateCustomerWithoutOptionalFields() public function testActivateCustomer() { $customerData = $this->_createCustomer(); - $this->assertNotNull($customerData[Customer::CONFIRMATION], 'Customer activation is not required'); + + // Update the customer's confirmation key to a known value + $customerData = $this->customerHelper->updateSampleCustomer( + $customerData[Customer::ID], + [ + 'id' => $customerData[Customer::ID], + 'confirmation' => CustomerHelper::CONFIRMATION + ] + ); $serviceInfo = [ 'rest' => [ @@ -265,16 +273,15 @@ public function testActivateCustomer() $requestData = [ 'email' => $customerData[Customer::EMAIL], - 'confirmationKey' => $customerData[Customer::CONFIRMATION], + 'confirmationKey' => CustomerHelper::CONFIRMATION ]; - $result = $this->_webApiCall($serviceInfo, $requestData); - - $this->assertEquals($customerData[Customer::ID], $result[Customer::ID], 'Wrong customer!'); - $this->assertTrue( - !isset($result[Customer::CONFIRMATION]) || $result[Customer::CONFIRMATION] === null, - 'Customer is not activated!' - ); + try { + $result = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals($customerData[Customer::ID], $result[Customer::ID], 'Wrong customer!'); + } catch (\Exception $e) { + $this->fail('Customer is not activated.'); + } } public function testGetCustomerActivateCustomer() @@ -294,14 +301,15 @@ public function testGetCustomerActivateCustomer() ]; $requestData = [ 'email' => $customerData[Customer::EMAIL], - 'confirmationKey' => $customerData[Customer::CONFIRMATION], + 'confirmationKey' => CustomerHelper::CONFIRMATION ]; - $customerResponseData = $this->_webApiCall($serviceInfo, $requestData); - - $this->assertEquals($customerData[Customer::ID], $customerResponseData[Customer::ID]); - // Confirmation key is removed after confirmation - $this->assertFalse(isset($customerResponseData[Customer::CONFIRMATION])); + try { + $customerResponseData = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals($customerData[Customer::ID], $customerResponseData[Customer::ID]); + } catch (\Exception $e) { + $this->fail('Customer is not activated.'); + } } public function testValidateResetPasswordLinkToken() diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php index 748947825ba09..fbf131bf4deca 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AddressMetadataTest.php @@ -6,8 +6,11 @@ namespace Magento\Customer\Api; +use Magento\Config\Model\ResourceModel\Config; use Magento\Customer\Api\Data\AddressInterface as Address; use Magento\Customer\Model\Data\AttributeMetadata; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -19,15 +22,40 @@ class AddressMetadataTest extends WebapiAbstract const SERVICE_VERSION = "V1"; const RESOURCE_PATH = "/V1/attributeMetadata/customerAddress"; + /** + * @var Config $config + */ + private $resourceConfig; + + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + $this->resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + } + /** * Test retrieval of attribute metadata for the address entity type. * * @param string $attributeCode The attribute code of the requested metadata. * @param array $expectedMetadata Expected entity metadata for the attribute code. * @dataProvider getAttributeMetadataDataProvider + * @magentoDbIsolation disabled */ - public function testGetAttributeMetadata($attributeCode, $expectedMetadata) + public function testGetAttributeMetadata($attributeCode, $configOptions, $expectedMetadata) { + $this->initConfig($configOptions); + $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/attribute/$attributeCode", @@ -54,12 +82,14 @@ public function testGetAttributeMetadata($attributeCode, $expectedMetadata) * Data provider for testGetAttributeMetadata. * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getAttributeMetadataDataProvider() { return [ Address::POSTCODE => [ Address::POSTCODE, + [], [ AttributeMetadata::FRONTEND_INPUT => 'text', AttributeMetadata::INPUT_FILTER => '', @@ -83,7 +113,85 @@ public function getAttributeMetadataDataProvider() AttributeMetadata::IS_SEARCHABLE_IN_GRID => true, AttributeMetadata::ATTRIBUTE_CODE => 'postcode', ], - ] + ], + 'prefix' => [ + 'prefix', + [ + ['path' => 'customer/address/prefix_show', 'value' => 'opt'], + ['path' => 'customer/address/prefix_options', 'value' => 'prefA;prefB'] + ], + [ + AttributeMetadata::FRONTEND_INPUT => 'text', + AttributeMetadata::INPUT_FILTER => '', + AttributeMetadata::STORE_LABEL => 'Name Prefix', + AttributeMetadata::MULTILINE_COUNT => 0, + AttributeMetadata::VALIDATION_RULES => [], + AttributeMetadata::VISIBLE => false, + AttributeMetadata::REQUIRED => false, + AttributeMetadata::DATA_MODEL => '', + AttributeMetadata::OPTIONS => [ + [ + 'label' => 'prefA', + 'value' => 'prefA', + ], + [ + 'label' => 'prefB', + 'value' => 'prefB', + ], + ], + AttributeMetadata::FRONTEND_CLASS => '', + AttributeMetadata::USER_DEFINED => false, + AttributeMetadata::SORT_ORDER => 10, + AttributeMetadata::FRONTEND_LABEL => 'Name Prefix', + AttributeMetadata::NOTE => '', + AttributeMetadata::SYSTEM => false, + AttributeMetadata::BACKEND_TYPE => 'static', + AttributeMetadata::IS_USED_IN_GRID => false, + AttributeMetadata::IS_VISIBLE_IN_GRID => false, + AttributeMetadata::IS_FILTERABLE_IN_GRID => false, + AttributeMetadata::IS_SEARCHABLE_IN_GRID => false, + AttributeMetadata::ATTRIBUTE_CODE => 'prefix', + ], + ], + 'suffix' => [ + 'suffix', + [ + ['path' => 'customer/address/suffix_show', 'value' => 'opt'], + ['path' => 'customer/address/suffix_options', 'value' => 'suffA;suffB'] + ], + [ + AttributeMetadata::FRONTEND_INPUT => 'text', + AttributeMetadata::INPUT_FILTER => '', + AttributeMetadata::STORE_LABEL => 'Name Suffix', + AttributeMetadata::MULTILINE_COUNT => 0, + AttributeMetadata::VALIDATION_RULES => [], + AttributeMetadata::VISIBLE => false, + AttributeMetadata::REQUIRED => false, + AttributeMetadata::DATA_MODEL => '', + AttributeMetadata::OPTIONS => [ + [ + 'label' => 'suffA', + 'value' => 'suffA', + ], + [ + 'label' => 'suffB', + 'value' => 'suffB', + ], + ], + AttributeMetadata::FRONTEND_CLASS => '', + AttributeMetadata::USER_DEFINED => false, + AttributeMetadata::SORT_ORDER => 50, + AttributeMetadata::FRONTEND_LABEL => 'Name Suffix', + AttributeMetadata::NOTE => '', + AttributeMetadata::SYSTEM => false, + AttributeMetadata::BACKEND_TYPE => 'static', + AttributeMetadata::IS_USED_IN_GRID => false, + AttributeMetadata::IS_VISIBLE_IN_GRID => false, + AttributeMetadata::IS_FILTERABLE_IN_GRID => false, + AttributeMetadata::IS_SEARCHABLE_IN_GRID => false, + AttributeMetadata::ATTRIBUTE_CODE => 'suffix', + ], + ], ]; } @@ -106,7 +214,7 @@ public function testGetAllAttributesMetadata() $attributeMetadata = $this->_webApiCall($serviceInfo); $this->assertCount(19, $attributeMetadata); - $postcode = $this->getAttributeMetadataDataProvider()[Address::POSTCODE][1]; + $postcode = $this->getAttributeMetadataDataProvider()[Address::POSTCODE][2]; $validationResult = $this->checkMultipleAttributesValidationRules($postcode, $attributeMetadata); list($postcode, $attributeMetadata) = $validationResult; $this->assertContains($postcode, $attributeMetadata); @@ -187,7 +295,7 @@ public function getAttributesDataProvider() return [ [ 'customer_address_edit', - $attributeMetadata[Address::POSTCODE][1], + $attributeMetadata[Address::POSTCODE][2], ] ]; } @@ -200,6 +308,7 @@ public function getAttributesDataProvider() * @param array $actualResult * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ public function checkValidationRules($expectedResult, $actualResult) { @@ -235,6 +344,7 @@ public function checkValidationRules($expectedResult, $actualResult) } return [$expectedResult, $actualResult]; } + //phpcs:enable /** * Check specific attribute validation rules in set of multiple attributes @@ -277,4 +387,19 @@ public static function tearDownAfterClass() $attribute->delete(); } } + + /** + * Set core config data. + * + * @param $configOptions + */ + private function initConfig(array $configOptions): void + { + if ($configOptions) { + foreach ($configOptions as $option) { + $this->resourceConfig->saveConfig($option['path'], $option['value']); + } + } + $this->reinitConfig->reinit(); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php index 709abbbb8fbf9..7a02e2f843719 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -10,6 +10,9 @@ use Magento\Customer\Api\Data\AddressInterface as Address; use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -780,6 +783,66 @@ public function testSearchCustomersMultipleFilterGroups() $this->assertEquals(0, $searchResults['total_count']); } + /** + * Test revoking all access Tokens for customer + */ + public function testRevokeAllAccessTokensForCustomer() + { + $customerData = $this->_createCustomer(); + + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = Bootstrap::getObjectManager()->create(CustomerTokenServiceInterface::class); + $token = $customerTokenService->createCustomerAccessToken( + $customerData[Customer::EMAIL], + CustomerHelper::PASSWORD + ); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/me', + 'httpMethod' => Request::HTTP_METHOD_GET, + 'token' => $token, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'GetSelf', + 'token' => $token, + ], + ]; + + $customerLoadedData = $this->_webApiCall($serviceInfo, ['customerId' => $customerData[Customer::ID]]); + self::assertGreaterThanOrEqual($customerData[Customer::UPDATED_AT], $customerLoadedData[Customer::UPDATED_AT]); + unset($customerData[Customer::UPDATED_AT]); + self::assertArraySubset($customerData, $customerLoadedData); + + $revokeToken = $customerTokenService->revokeCustomerAccessToken($customerData[Customer::ID]); + self::assertTrue($revokeToken); + + try { + $customerTokenService->revokeCustomerAccessToken($customerData[Customer::ID]); + } catch (\Throwable $exception) { + $this->assertInstanceOf(LocalizedException::class, $exception); + $this->assertEquals('This customer has no tokens.', $exception->getMessage()); + } + + $expectedMessage = 'The consumer isn\'t authorized to access %resources.'; + + try { + $this->_webApiCall($serviceInfo, ['customerId' => $customerData[Customer::ID]]); + } catch (\SoapFault $e) { + $this->assertContains( + $expectedMessage, + $e->getMessage(), + 'SoapFault does not contain expected message.' + ); + } catch (\Throwable $e) { + $errorObj = $this->processRestExceptionResult($e); + $this->assertEquals($expectedMessage, $errorObj['message']); + $this->assertEquals(['resources' => 'self'], $errorObj['parameters']); + $this->assertEquals(HTTPExceptionCodes::HTTP_UNAUTHORIZED, $e->getCode()); + } + } + /** * Retrieve customer data by Id * diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/LinkRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/LinkRepositoryTest.php index c881969a3b679..3a24aab30cb65 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/LinkRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/LinkRepositoryTest.php @@ -12,6 +12,9 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * API tests for Magento\Downloadable\Model\LinkRepository. + */ class LinkRepositoryTest extends WebapiAbstract { /** @@ -135,10 +138,12 @@ public function testCreateUploadsProvidedFileContent() 'number_of_downloads' => 100, 'link_type' => 'file', 'link_file_content' => [ + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), 'name' => 'image.jpg', ], 'sample_file_content' => [ + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), 'name' => 'image.jpg', ], @@ -292,6 +297,64 @@ public function testCreateThrowsExceptionIfLinkFileContentIsNotAValidBase64Encod $this->_webApiCall($this->createServiceInfo, $requestData); } + /** + * Check that error appears when link file not existing in filesystem. + * + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + * @expectedException \Exception + * @expectedExceptionMessage Link file not found. Please try again. + * @return void + */ + public function testCreateLinkWithMissingLinkFileThrowsException(): void + { + $requestData = [ + 'isGlobalScopeContent' => false, + 'sku' => 'downloadable-product', + 'link' => [ + 'title' => 'Link Title', + 'sort_order' => 1, + 'price' => 10, + 'is_shareable' => 1, + 'number_of_downloads' => 100, + 'link_type' => 'file', + 'link_file' => '/n/o/nexistfile.png', + 'sample_type' => 'url', + 'sample_file' => 'http://google.com', + ], + ]; + + $this->_webApiCall($this->createServiceInfo, $requestData); + } + + /** + * Check that error appears when link sample file not existing in filesystem. + * + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + * @expectedException \Exception + * @expectedExceptionMessage Link sample file not found. Please try again. + * @return void + */ + public function testCreateLinkWithMissingSampleFileThrowsException(): void + { + $requestData = [ + 'isGlobalScopeContent' => false, + 'sku' => 'downloadable-product', + 'link' => [ + 'title' => 'Link Title', + 'sort_order' => 1, + 'price' => 10, + 'is_shareable' => 1, + 'number_of_downloads' => 100, + 'link_type' => 'url', + 'link_url' => 'http://www.example.com/', + 'sample_type' => 'file', + 'sample_file' => '/n/o/nexistfile.png', + ], + ]; + + $this->_webApiCall($this->createServiceInfo, $requestData); + } + /** * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php * @expectedException \Exception @@ -339,6 +402,7 @@ public function testCreateThrowsExceptionIfLinkFileNameContainsForbiddenCharacte 'number_of_downloads' => 100, 'link_type' => 'file', 'link_file_content' => [ + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), 'name' => 'name/with|forbidden{characters', ], @@ -370,6 +434,7 @@ public function testCreateThrowsExceptionIfSampleFileNameContainsForbiddenCharac 'link_url' => 'http://www.example.com/', 'sample_type' => 'file', 'sample_file_content' => [ + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), 'name' => 'name/with|forbidden{characters', ], @@ -405,6 +470,58 @@ public function testCreateThrowsExceptionIfLinkUrlHasWrongFormat() $this->_webApiCall($this->createServiceInfo, $requestData); } + /** + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + * @expectedException \Exception + * @expectedExceptionMessage Link URL's domain is not in list of downloadable_domains in env.php. + */ + public function testCreateThrowsExceptionIfLinkUrlUsesDomainNotInWhitelist() + { + $requestData = [ + 'isGlobalScopeContent' => false, + 'sku' => 'downloadable-product', + 'link' => [ + 'title' => 'Link Title', + 'sort_order' => 1, + 'price' => 10, + 'is_shareable' => 1, + 'number_of_downloads' => 100, + 'link_type' => 'url', + 'link_url' => 'http://notAnywhereInEnv.com/', + 'sample_type' => 'url', + 'sample_url' => 'http://www.example.com/', + ], + ]; + + $this->_webApiCall($this->createServiceInfo, $requestData); + } + + /** + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + * @expectedException \Exception + * @expectedExceptionMessage Sample URL's domain is not in list of downloadable_domains in env.php. + */ + public function testCreateThrowsExceptionIfSampleUrlUsesDomainNotInWhitelist() + { + $requestData = [ + 'isGlobalScopeContent' => false, + 'sku' => 'downloadable-product', + 'link' => [ + 'title' => 'Link Title', + 'sort_order' => 1, + 'price' => 10, + 'is_shareable' => 1, + 'number_of_downloads' => 100, + 'link_type' => 'url', + 'link_url' => 'http://example.com/', + 'sample_type' => 'url', + 'sample_url' => 'http://www.notAnywhereInEnv.com/', + ], + ]; + + $this->_webApiCall($this->createServiceInfo, $requestData); + } + /** * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php * @expectedException \Exception @@ -610,7 +727,9 @@ public function testUpdate() 'is_shareable' => 0, 'number_of_downloads' => 50, 'link_type' => 'url', + 'link_url' => 'http://google.com', 'sample_type' => 'url', + 'sample_url' => 'http://google.com', ], ]; $this->assertEquals($linkId, $this->_webApiCall($this->updateServiceInfo, $requestData)); @@ -643,7 +762,9 @@ public function testUpdateSavesDataInGlobalScopeAndDoesNotAffectValuesStoredInSt 'is_shareable' => 0, 'number_of_downloads' => 50, 'link_type' => 'url', + 'link_url' => 'http://google.com', 'sample_type' => 'url', + 'sample_url' => 'http://google.com', ], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php index 769abadf20585..c2393d0a5ad2d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php @@ -27,7 +27,14 @@ class ProductRepositoryTest extends WebapiAbstract protected function setUp() { + parent::setUp(); $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; + + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + /** @var DomainManagerInterface $domainManager */ + $domainManager = $objectManager->get(DomainManagerInterface::class); + $domainManager->addDomains(['www.example.com']); } /** @@ -37,6 +44,12 @@ public function tearDown() { $this->deleteProductBySku(self::PRODUCT_SKU); parent::tearDown(); + + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + /** @var DomainManagerInterface $domainManager */ + $domainManager = $objectManager->get(DomainManagerInterface::class); + $domainManager->removeDomains(['www.example.com']); } protected function getLinkData() @@ -227,7 +240,9 @@ public function testUpdateDownloadableProductLinks() 'price' => 5.0, 'number_of_downloads' => 999, 'link_type' => 'file', - 'sample_type' => 'file' + 'link_file' => $linkFile, + 'sample_type' => 'file', + 'sample_file' => $sampleFile, ]; $linkData = $this->getLinkData(); diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/SampleRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/SampleRepositoryTest.php index b537947d5e4db..a97e4c5d9e119 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/SampleRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/SampleRepositoryTest.php @@ -11,6 +11,9 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * API tests for Magento\Downloadable\Model\SampleRepository. + */ class SampleRepositoryTest extends WebapiAbstract { /** @@ -131,6 +134,7 @@ public function testCreateUploadsProvidedFileContent() 'title' => 'Title', 'sort_order' => 1, 'sample_file_content' => [ + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), 'name' => 'image.jpg', ], @@ -223,6 +227,30 @@ public function testCreateThrowsExceptionIfSampleTypeIsInvalid() $this->_webApiCall($this->createServiceInfo, $requestData); } + /** + * Check that error appears when sample file not existing in filesystem. + * + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + * @expectedException \Exception + * @expectedExceptionMessage Sample file not found. Please try again. + * @return void + */ + public function testCreateSampleWithMissingFileThrowsException(): void + { + $requestData = [ + 'isGlobalScopeContent' => false, + 'sku' => 'downloadable-product', + 'sample' => [ + 'title' => 'Link Title', + 'sort_order' => 1, + 'sample_type' => 'file', + 'sample_file' => '/n/o/nexistfile.png', + ], + ]; + + $this->_webApiCall($this->createServiceInfo, $requestData); + } + /** * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php * @expectedException \Exception @@ -262,6 +290,7 @@ public function testCreateThrowsExceptionIfSampleFileNameContainsForbiddenCharac 'sort_order' => 15, 'sample_type' => 'file', 'sample_file_content' => [ + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), 'name' => 'name/with|forbidden{characters', ], @@ -380,6 +409,7 @@ public function testUpdate() 'title' => 'Updated Title', 'sort_order' => 2, 'sample_type' => 'url', + 'sample_url' => 'http://google.com', ], ]; @@ -408,6 +438,7 @@ public function testUpdateSavesDataInGlobalScopeAndDoesNotAffectValuesStoredInSt 'title' => 'Updated Title', 'sort_order' => 2, 'sample_type' => 'url', + 'sample_url' => 'http://google.com', ], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php index 65ea71bd34937..e6bc36684ed80 100644 --- a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php @@ -38,7 +38,7 @@ public function testCatalogSearch() ] ] ], - 'page_size' => 20000000000000, + 'page_size' => 999, 'current_page' => 0, ], ]; @@ -66,7 +66,15 @@ public function testCatalogSearch() $this->assertTrue(count($response['items']) > 0); $this->assertNotNull($response['items'][0]['id']); - $this->assertEquals('score', $response['items'][0]['custom_attributes'][0]['attribute_code']); + $this->assertTrue( + in_array( + $response['items'][0]['custom_attributes'][0]['attribute_code'], + [ + 'score', // mysql + '_score' // elasticsearch score + ] + ) + ); $this->assertTrue($response['items'][0]['custom_attributes'][0]['value'] > 0); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php index ad454c67080e9..e9ab4456fae81 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php @@ -109,12 +109,208 @@ public function dataProviderTestPlaceOrder(): array ]; } + /** + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/active 1 + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login def_login + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key def_trans_key + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/public_client_key def_public_client_key + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key def_trans_signature_key + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @dataProvider dataProviderSetPaymentInvalidInput + * @param \Closure $getMutationClosure + * @param array $expectedMessages + * @expectedException \Exception + */ + public function testSetPaymentInvalidInput(\Closure $getMutationClosure, array $expectedMessages) + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $setPaymentMutation = $getMutationClosure($maskedQuoteId); + + foreach ($expectedMessages as $expectedMessage) { + $this->expectExceptionMessage($expectedMessage); + } + $this->graphQlMutation($setPaymentMutation, [], '', $this->getHeaderMap()); + } + + /** + * Data provider for testSetPaymentInvalidInput + * + * @return array + */ + public function dataProviderSetPaymentInvalidInput(): array + { + return [ + [ + function (string $maskedQuoteId) { + return $this->getInvalidSetPaymentMutation($maskedQuoteId); + }, + [ + 'Required parameter "authorizenet_acceptjs" for "payment_method" is missing.' + ] + ], + [ + function (string $maskedQuoteId) { + return $this->getEmptyAcceptJsInput($maskedQuoteId); + }, + [ + 'Field AuthorizenetInput.cc_last_4 of required type Int! was not provided.', + 'Field AuthorizenetInput.opaque_data_descriptor of required type String! was not provided.', + 'Field AuthorizenetInput.opaque_data_value of required type String! was not provided.' + ] + + ], + [ + function (string $maskedQuoteId) { + return $this->getMissingCcLastFourAcceptJsInput( + $maskedQuoteId, + static::VALID_DESCRIPTOR, + static::VALID_NONCE + ); + }, + [ + 'Field AuthorizenetInput.cc_last_4 of required type Int! was not provided', + ] + ], + [ + function (string $maskedQuoteId) { + return $this->getMissingOpaqueDataValueAcceptJsInput($maskedQuoteId, static::VALID_DESCRIPTOR); + }, + [ + 'Field AuthorizenetInput.opaque_data_value of required type String! was not provided', + ] + ], + ]; + } + + /** + * Get setPaymentMethodOnCart missing additional data property + * + * @param string $maskedQuoteId + * @return string + */ + private function getInvalidSetPaymentMutation(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"authorizenet_acceptjs" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + + /** + * Get setPaymentMethodOnCart missing required additional data properties + * + * @param string $maskedQuoteId + * @return string + */ + private function getEmptyAcceptJsInput(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"authorizenet_acceptjs" + authorizenet_acceptjs: {} + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + + /** + * Get setPaymentMethodOnCart missing required additional data properties + * + * @param string $maskedQuoteId + * @return string + */ + private function getMissingCcLastFourAcceptJsInput(string $maskedQuoteId, string $descriptor, string $nonce): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"authorizenet_acceptjs" + authorizenet_acceptjs:{ + opaque_data_descriptor: "{$descriptor}" + opaque_data_value: "{$nonce}" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + + /** + * Get setPaymentMethodOnCart missing required additional data properties + * + * @param string $maskedQuoteId + * @return string + */ + private function getMissingOpaqueDataValueAcceptJsInput(string $maskedQuoteId, string $descriptor): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"authorizenet_acceptjs" + authorizenet_acceptjs:{ + opaque_data_descriptor: "{$descriptor}" + cc_last_4: 1111 + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + private function assertPlaceOrderResponse(array $response, string $reservedOrderId): void { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -171,7 +367,7 @@ private function getPlaceOrderMutation(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php index 3bd7ade23ae4b..322d984f5fa75 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php @@ -113,8 +113,8 @@ private function assertPlaceOrderResponse(array $response, string $reservedOrder { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -171,7 +171,7 @@ private function getPlaceOrderMutation(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php index 1564d00fa5996..7d69c49ae6aa3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php @@ -20,6 +20,7 @@ class CreateBraintreeClientTokenTest extends GraphQlAbstract * @magentoConfigFixture default_store payment/braintree/active 1 * @magentoConfigFixture default_store payment/braintree/environment sandbox * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id + * @magentoConfigFixture default_store payment/braintree/merchant_account_id def_merchant_id * @magentoConfigFixture default_store payment/braintree/public_key def_public_key * @magentoConfigFixture default_store payment/braintree/private_key def_private_key */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php index 84a639af30b0e..a282b295c2974 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php @@ -8,6 +8,7 @@ namespace Magento\GraphQl\Braintree\Customer; use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Framework\Exception\AuthenticationException; use Magento\Framework\Registry; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; @@ -258,6 +259,43 @@ public function testSetPaymentMethodInvalidMethodInput(string $methodCode) $methodCode ); $this->expectExceptionMessage("for \"$methodCode\" is missing."); + $expectedExceptionMessages = [ + 'braintree' => + 'Field BraintreeInput.is_active_payment_token_enabler of required type Boolean! was not provided.', + 'braintree_cc_vault' => + 'Field BraintreeCcVaultInput.public_hash of required type String! was not provided.' + ]; + + $this->expectExceptionMessage($expectedExceptionMessages[$methodCode]); + $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap()); + } + + /** + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store payment/braintree/active 1 + * @magentoConfigFixture default_store payment/braintree_cc_vault/active 1 + * @magentoConfigFixture default_store payment/braintree/environment sandbox + * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id + * @magentoConfigFixture default_store payment/braintree/public_key def_public_key + * @magentoConfigFixture default_store payment/braintree/private_key def_private_key + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @expectedException \Exception + */ + public function testSetPaymentMethodWithoutRequiredPaymentMethodInput() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidPaymentMethodInput($maskedQuoteId); + $this->expectExceptionMessage( + 'Field BraintreeInput.is_active_payment_token_enabler of required type Boolean! was not provided.' + ); $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap()); } @@ -273,8 +311,8 @@ private function assertPlaceOrderResponse(array $response, string $reservedOrder { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -371,6 +409,33 @@ private function getSetPaymentBraintreeQueryInvalidInput(string $maskedQuoteId, QUERY; } + /** + * @param string $maskedQuoteId + * @return string + */ + private function getSetPaymentBraintreeQueryInvalidPaymentMethodInput(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"braintree" + braintree:{ + payment_method_nonce:"fake-valid-nonce" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + /** * @param string $maskedQuoteId * @param string $methodCode @@ -407,7 +472,7 @@ private function getPlaceOrderQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } @@ -437,7 +502,7 @@ private function getPaymentTokenQuery(): string * @param string $username * @param string $password * @return array - * @throws \Magento\Framework\Exception\AuthenticationException + * @throws AuthenticationException */ private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php index 1d48c5253fe84..c0a7491cbc1bf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php @@ -151,7 +151,14 @@ public function testSetPaymentMethodInvalidMethodInput() $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidMethodInput($maskedQuoteId); - $this->expectExceptionMessage("for \"braintree\" is missing."); + + $this->expectExceptionMessage( + 'Field BraintreeInput.is_active_payment_token_enabler of required type Boolean! was not provided' + ); + $this->expectExceptionMessage( + 'Field BraintreeInput.payment_method_nonce of required type String! was not provided.' + ); + $this->graphQlMutation($setPaymentQuery); } @@ -159,8 +166,8 @@ private function assertPlaceOrderResponse(array $response, string $reservedOrder { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -259,7 +266,7 @@ private function getPlaceOrderQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php new file mode 100644 index 0000000000000..826083b0b3378 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php @@ -0,0 +1,274 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Bundle; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +/** + * Test adding bundled products to cart + */ +class AddBundleProductToCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var Quote + */ + private $quote; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quote = $objectManager->create(Quote::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleProductToCart() + { + $sku = 'bundle-product'; + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $product = $this->productRepository->get($sku); + + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var $option \Magento\Bundle\Model\Option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var \Magento\Catalog\Model\Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$maskedQuoteId}" + cart_items:[ + { + data:{ + sku:"{$sku}" + quantity:1 + } + bundle_options:[ + { + id:{$optionId} + quantity:1 + value:[ + "{$selectionId}" + ] + } + ] + } + ] + }) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('addBundleProductsToCart', $response); + $this->assertArrayHasKey('cart', $response['addBundleProductsToCart']); + $cart = $response['addBundleProductsToCart']['cart']; + $bundleItem = current($cart['items']); + $this->assertEquals($sku, $bundleItem['product']['sku']); + $bundleItemOption = current($bundleItem['bundle_options']); + $this->assertEquals($optionId, $bundleItemOption['id']); + $this->assertEquals($option->getTitle(), $bundleItemOption['label']); + $this->assertEquals($option->getType(), $bundleItemOption['type']); + $value = current($bundleItemOption['values']); + $this->assertEquals($selection->getSelectionId(), $value['id']); + $this->assertEquals((float) $selection->getSelectionPriceValue(), $value['price']); + $this->assertEquals(1, $value['quantity']); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/quote_with_bundle_and_options.php + * @dataProvider dataProviderTestUpdateBundleItemQuantity + */ + public function testUpdateBundleItemQuantity(int $quantity) + { + $this->quoteResource->load( + $this->quote, + 'test_cart_with_bundle_and_options', + 'reserved_order_id' + ); + + $item = current($this->quote->getAllVisibleItems()); + + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $mutation = <<<QUERY +mutation { + updateCartItems( + input: { + cart_id: "{$maskedQuoteId}" + cart_items: { + cart_item_id: {$item->getId()} + quantity: {$quantity} + } + } + ) { + cart { + items { + id + quantity + product { + sku + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($mutation); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + $cart = $response['updateCartItems']['cart']; + if ($quantity === 0) { + $this->assertCount(0, $cart['items']); + return; + } + + $bundleItem = current($cart['items']); + $this->assertEquals($quantity, $bundleItem['quantity']); + } + + public function dataProviderTestUpdateBundleItemQuantity(): array + { + return [ + [2], + [0], + ]; + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Please select all required options + */ + public function testAddBundleToCartWithoutOptions() + { + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$maskedQuoteId}" + cart_items:[ + { + data:{ + sku:"bundle-product" + quantity:1 + } + bundle_options:[ + { + id:555 + quantity:1 + value:[ + "555" + ] + } + ] + } + ] + }) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} +QUERY; + + $this->graphQlMutation($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryCanonicalUrlTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryCanonicalUrlTest.php new file mode 100644 index 0000000000000..794df3a8b6f44 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryCanonicalUrlTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\Data\CategoryInterface; + +/** + * Test for getting canonical url data from category + */ +class CategoryCanonicalUrlTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @magentoConfigFixture default_store catalog/seo/category_canonical_tag 1 + */ + public function testCategoryWithCanonicalLinksMetaTagSettingsEnabled() + { + $this->objectManager = Bootstrap::getObjectManager(); + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->objectManager->create(CategoryCollection::class); + $categoryCollection->addFieldToFilter('name', 'Category 1.1.1'); + /** @var CategoryInterface $category */ + $category = $categoryCollection->getFirstItem(); + $categoryId = $category->getId(); + $query = <<<QUERY + { +categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + url_suffix + canonical_url + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['categoryList'], 'Category list should not be empty'); + $this->assertEquals('.html', $response['categoryList'][0]['url_suffix']); + $this->assertEquals( + 'category-1/category-1-1/category-1-1-1.html', + $response['categoryList'][0]['canonical_url'] + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @magentoConfigFixture default_store catalog/seo/category_canonical_tag 0 + */ + public function testCategoryWithCanonicalLinksMetaTagSettingsDisabled() + { + $this->objectManager = Bootstrap::getObjectManager(); + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->objectManager->create(CategoryCollection::class); + $categoryCollection->addFieldToFilter('name', 'Category 1.1'); + /** @var CategoryInterface $category */ + $category = $categoryCollection->getFirstItem(); + $categoryId = $category->getId(); + $query = <<<QUERY + { +categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + canonical_url + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['categoryList'], 'Category list should not be empty'); + $this->assertNull( + $response['categoryList'][0]['canonical_url'] + ); + $this->assertEquals('category-1-1', $response['categoryList'][0]['url_key']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php new file mode 100644 index 0000000000000..96e8ae79b612e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -0,0 +1,556 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test CategoryList GraphQl query + */ +class CategoryListTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @dataProvider filterSingleCategoryDataProvider + * @param $field + * @param $condition + * @param $value + */ + public function testFilterSingleCategoryByField($field, $condition, $value, $expectedResult) + { + $query = <<<QUERY +{ + categoryList(filters: { $field : { $condition : "$value" } }){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertResponseFields($result['categoryList'][0], $expectedResult); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @dataProvider filterMultipleCategoriesDataProvider + * @param $field + * @param $condition + * @param $value + * @param $expectedResult + */ + public function testFilterMultipleCategoriesByField($field, $condition, $value, $expectedResult) + { + $query = <<<QUERY +{ + categoryList(filters: { $field : { $condition : $value } }){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(count($expectedResult), $result['categoryList']); + foreach ($expectedResult as $i => $expected) { + $this->assertResponseFields($result['categoryList'][$i], $expected); + } + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryByMultipleFields() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["6","7","8","9","10"]}, name: {match: "Movable"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(3, $result['categoryList']); + + $expectedCategories = [7 => 'Movable', 9 => 'Movable Position 1', 10 => 'Movable Position 2']; + $actualCategories = array_column($result['categoryList'], 'name', 'id'); + $this->assertEquals($expectedCategories, $actualCategories); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterWithInactiveCategory() + { + $query = <<<QUERY +{ + categoryList(filters: {url_key: {in: ["inactive", "category-2"]}}){ + id + name + url_key + url_path + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $actualCategories = array_column($result['categoryList'], 'url_key', 'id'); + $this->assertContains('category-2', $actualCategories); + $this->assertNotContains('inactive', $actualCategories); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testQueryChildCategoriesWithProducts() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["3"]}}){ + id + name + url_key + url_path + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + url_key + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + } + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $baseCategory = $result['categoryList'][0]; + + $this->assertEquals('Category 1', $baseCategory['name']); + $this->assertArrayHasKey('products', $baseCategory); + //Check base category products + $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); + //Check base category children + $expectedBaseCategoryChildren = [ + ['name' => 'Category 1.1', 'description' => 'Category 1.1 description.'], + ['name' => 'Category 1.2', 'description' => 'Its a description of Test Category 1.2'] + ]; + $this->assertCategoryChildren($baseCategory, $expectedBaseCategoryChildren); + + //Check first child category + $firstChildCategory = $baseCategory['children'][0]; + $this->assertEquals('Category 1.1', $firstChildCategory['name']); + $this->assertEquals('Category 1.1 description.', $firstChildCategory['description']); + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ]; + $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = [['name' =>'Category 1.1.1']]; + $this->assertCategoryChildren($firstChildCategory, $firstChildCategoryChildren); + //Check second child category + $secondChildCategory = $baseCategory['children'][1]; + $this->assertEquals('Category 1.2', $secondChildCategory['name']); + $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = []; + $this->assertCategoryChildren($secondChildCategory, $firstChildCategoryChildren); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories_disabled.php + */ + public function testQueryCategoryWithDisabledChildren() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["3"]}}){ + id + name + image + url_key + url_path + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + image + url_key + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + image + children{ + name + image + } + } + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $baseCategory = $result['categoryList'][0]; + + $this->assertEquals('Category 1', $baseCategory['name']); + $this->assertArrayHasKey('products', $baseCategory); + //Check base category products + $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); + //Check base category children + $expectedBaseCategoryChildren = [ + ['name' => 'Category 1.2', 'description' => 'Its a description of Test Category 1.2'] + ]; + $this->assertCategoryChildren($baseCategory, $expectedBaseCategoryChildren); + + //Check first child category + $firstChildCategory = $baseCategory['children'][0]; + $this->assertEquals('Category 1.2', $firstChildCategory['name']); + $this->assertEquals('Its a description of Test Category 1.2', $firstChildCategory['description']); + + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = []; + $this->assertCategoryChildren($firstChildCategory, $firstChildCategoryChildren); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testNoResultsFound() + { + $query = <<<QUERY +{ + categoryList(filters: {url_key: {in: ["inactive", "does-not-exist"]}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals([], $result['categoryList']); + } + + /** + * When no filters are supplied, the root category is returned + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testEmptyFiltersReturnRootCategory() + { + $query = <<<QUERY +{ + categoryList{ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + $storeRootCategoryId = $storeManager->getStore()->getRootCategoryId(); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals('Default Category', $result['categoryList'][0]['name']); + $this->assertEquals($storeRootCategoryId, $result['categoryList'][0]['id']); + } + + /** + * Filtering with match value less than minimum query should return empty result + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testMinimumMatchQueryLength() + { + $query = <<<QUERY +{ + categoryList(filters: {name: {match: "mo"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals([], $result['categoryList']); + } + + /** + * @return array + */ + public function filterSingleCategoryDataProvider(): array + { + return [ + [ + 'ids', + 'eq', + '4', + [ + 'id' => '4', + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'name', + 'match', + 'Movable Position 2', + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ], + [ + 'url_key', + 'eq', + 'category-1-1-1', + [ + 'id' => '5', + 'name' => 'Category 1.1.1', + 'url_key' => 'category-1-1-1', + 'url_path' => 'category-1/category-1-1/category-1-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4/5', + 'position' => '1' + ] + ], + ]; + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function filterMultipleCategoriesDataProvider(): array + { + return[ + //Filter by multiple IDs + [ + 'ids', + 'in', + '["4", "9", "10"]', + [ + [ + 'id' => '4', + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], + //Filter by multiple url keys + [ + 'url_key', + 'in', + '["category-1-2", "movable"]', + [ + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' + ], + [ + 'id' => '13', + 'name' => 'Category 1.2', + 'url_key' => 'category-1-2', + 'url_path' => 'category-1/category-1-2', + 'children_count' => '0', + 'path' => '1/2/3/13', + 'position' => '2' + ] + ] + ], + //Filter by matching multiple names + [ + 'name', + 'match', + '"Position"', + [ + [ + 'id' => '9', + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ], + [ + 'id' => '11', + 'name' => 'Movable Position 3', + 'url_key' => 'movable-position-3', + 'url_path' => 'movable-position-3', + 'children_count' => '0', + 'path' => '1/2/11', + 'position' => '7' + ] + ] + ] + ]; + } + + /** + * Check category products + * + * @param array $category + * @param array $expectedProducts + */ + private function assertCategoryProducts(array $category, array $expectedProducts) + { + $this->assertEquals(count($expectedProducts), $category['products']['total_count']); + $this->assertCount(count($expectedProducts), $category['products']['items']); + $this->assertResponseFields($category['products']['items'], $expectedProducts); + } + + /** + * Check category child categories + * + * @param array $category + * @param array $expectedChildren + */ + private function assertCategoryChildren(array $category, array $expectedChildren) + { + $this->assertArrayHasKey('children', $category); + $this->assertCount(count($expectedChildren), $category['children']); + foreach ($expectedChildren as $i => $expectedChild) { + $this->assertResponseFields($category['children'][$i], $expectedChild); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index df8e399ce6c61..480388db98d2f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -8,14 +8,15 @@ namespace Magento\GraphQl\Catalog; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\DataObject; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\TestFramework\ObjectManager; /** * Test loading of category tree @@ -113,6 +114,80 @@ public function testCategoriesTree() ); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testRootCategoryTree() + { + $query = <<<QUERY +{ + category { + id + level + description + path + path_in_store + product_count + url_key + url_path + children { + id + description + available_sort_by + default_sort_by + image + level + children { + id + filter_price_range + description + image + meta_keywords + level + is_anchor + children { + level + id + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $responseDataObject = new DataObject($response); + //Some sort of smoke testing + self::assertEquals( + 'Its a description of Test Category 1.2', + $responseDataObject->getData('category/children/0/children/1/description') + ); + self::assertEquals( + 'default-category', + $responseDataObject->getData('category/url_key') + ); + self::assertEquals( + [], + $responseDataObject->getData('category/children/0/available_sort_by') + ); + self::assertEquals( + 'name', + $responseDataObject->getData('category/children/0/default_sort_by') + ); + self::assertCount( + 7, + $responseDataObject->getData('category/children') + ); + self::assertCount( + 2, + $responseDataObject->getData('category/children/0/children') + ); + self::assertEquals( + 13, + $responseDataObject->getData('category/children/0/children/1/id') + ); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php */ @@ -187,6 +262,25 @@ public function testGetDisabledCategory() $this->graphQlQuery($query); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @expectedException \Exception + * @expectedExceptionMessage Category doesn't exist + */ + public function testGetCategoryIdZero() + { + $categoryId = 0; + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + name + } +} +QUERY; + $this->graphQlQuery($query); + } + public function testNonExistentCategoryWithProductCount() { $query = <<<QUERY @@ -383,7 +477,7 @@ public function testAnchorCategory() { category(id: {$categoryId}) { name - products(sort: {sku: ASC}) { + products(sort: {name: DESC}) { total_count items { sku @@ -400,8 +494,8 @@ public function testAnchorCategory() 'total_count' => 3, 'items' => [ ['sku' => '12345'], - ['sku' => 'simple'], - ['sku' => 'simple-4'] + ['sku' => 'simple-4'], + ['sku' => 'simple'] ] ] ] @@ -409,6 +503,105 @@ public function testAnchorCategory() $this->assertEquals($expectedResponse, $response); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testBreadCrumbs() + { + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->objectManager->create(CategoryCollection::class); + $categoryCollection->addFieldToFilter('name', 'Category 1.1.1'); + /** @var CategoryInterface $category */ + $category = $categoryCollection->getFirstItem(); + $categoryId = $category->getId(); + $this->assertNotEmpty($categoryId, "Preconditions failed: category is not available."); + $query = <<<QUERY +{ + category(id: {$categoryId}) { + name + breadcrumbs { + category_id + category_name + category_level + category_url_key + category_url_path + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $expectedResponse = [ + 'category' => [ + 'name' => 'Category 1.1.1', + 'breadcrumbs' => [ + [ + 'category_id' => 3, + 'category_name' => "Category 1", + 'category_level' => 2, + 'category_url_key' => "category-1", + 'category_url_path' => "category-1" + ], + [ + 'category_id' => 4, + 'category_name' => "Category 1.1", + 'category_level' => 3, + 'category_url_key' => "category-1-1", + 'category_url_path' => "category-1/category-1-1" + ], + ] + ] + ]; + $this->assertEquals($expectedResponse, $response); + } + + /** + * Test category image is returned as full url (not relative path) + * + * @magentoApiDataFixture Magento/Catalog/_files/catalog_category_with_image.php + */ + public function testCategoryImage() + { + $categoryCollection = $this->objectManager->get(CategoryCollection::class); + $categoryModel = $categoryCollection + ->addAttributeToSelect('image') + ->addAttributeToFilter('name', ['eq' => 'Parent Image Category']) + ->getFirstItem(); + $categoryId = $categoryModel->getId(); + + $query = <<<QUERY + { +categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + image + children { + id + name + url_key + image + } + + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $response); + $this->assertNotEmpty($response['categoryList']); + $categoryList = $response['categoryList']; + $storeBaseUrl = $this->objectManager->get(StoreManagerInterface::class)->getStore()->getBaseUrl('media'); + $expectedImageUrl = rtrim($storeBaseUrl, '/'). '/' . ltrim($categoryModel->getImage(), '/'); + + $this->assertEquals($categoryId, $categoryList[0]['id']); + $this->assertEquals('Parent Image Category', $categoryList[0]['name']); + $this->assertEquals($expectedImageUrl, $categoryList[0]['image']); + + $childCategory = $categoryList[0]['children'][0]; + $this->assertEquals('Child Image Category', $childCategory['name']); + $this->assertEquals($expectedImageUrl, $childCategory['image']); + } + /** * @param ProductInterface $product * @param array $actualResponse @@ -419,8 +612,7 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'price', 'expected_value' => - [ + ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ 'amount' => [ 'value' => $product->getPrice(), diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php index e805bc940704a..b6687b4e171d3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php @@ -198,6 +198,6 @@ private function checkImageExists(string $url): bool curl_exec($connection); $responseStatus = curl_getinfo($connection, CURLINFO_HTTP_CODE); // phpcs:enable Magento2.Functions.DiscouragedFunction - return $responseStatus === 200 ? true : false; + return $responseStatus === 200; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php new file mode 100644 index 0000000000000..517a1c966b04d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeOptionsTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Eav\Api\Data\AttributeOptionInterface; + +class ProductAttributeOptionsTest extends GraphQlAbstract +{ + /** + * Test that custom attribute options are returned correctly + * + * @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php + */ + public function testCustomAttributeMetadataOptions() + { + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'dropdown_attribute'); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = []; + // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($i = 0; $i < count($options); $i++) { + $optionValues[] = $options[$i]->getValue(); + } + $query + = <<<QUERY +{ + customAttributeMetadata(attributes: + [ + { + attribute_code:"description", + entity_type:"catalog_product" + }, + { + attribute_code:"status", + entity_type:"catalog_product" + }, + { + attribute_code:"dropdown_attribute", + entity_type:"catalog_product" + } + ] + ) + { + items + { + attribute_code + attribute_type + entity_type + input_type + attribute_options{ + label + value + } + } + } + } +QUERY; + $response = $this->graphQlQuery($query); + + $expectedOptionArray = [ + [], // description attribute has no options + [ + [ + 'label' => 'Enabled', + 'value' => '1' + ], + [ + 'label' => 'Disabled', + 'value' => '2' + ] + ], + [ + [ + 'label' => 'Option 1', + 'value' => $optionValues[0] + ], + [ + 'label' => 'Option 2', + 'value' => $optionValues[1] + ], + [ + 'label' => 'Option 3', + 'value' => $optionValues[2] + ] + ] + ]; + + $this->assertNotEmpty($response['customAttributeMetadata']['items']); + $actualAttributes = $response['customAttributeMetadata']['items']; + + foreach ($expectedOptionArray as $index => $expectedOptions) { + $actualOption = $actualAttributes[$index]['attribute_options']; + $this->assertEquals($expectedOptions, $actualOption); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php index 063da7c11bf7f..a34d5e21704af 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php @@ -50,7 +50,8 @@ public function testAttributeTypeResolver() { attribute_code attribute_type - entity_type + entity_type + input_type } } } @@ -71,7 +72,8 @@ public function testAttributeTypeResolver() \Magento\Catalog\Api\Data\ProductInterface::class ]; $attributeTypes = ['String', 'Int', 'Float','Boolean', 'Float']; - $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $response); + $inputTypes = ['textarea', 'select', 'price', 'boolean', 'price']; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $inputTypes, $response); } /** @@ -121,7 +123,8 @@ public function testComplexAttributeTypeResolver() { attribute_code attribute_type - entity_type + entity_type + input_type } } } @@ -154,7 +157,16 @@ public function testComplexAttributeTypeResolver() 'CustomerDataRegionInterface', 'ProductMediaGallery' ]; - $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $response); + $inputTypes = [ + 'select', + 'multiselect', + 'select', + 'select', + 'text', + 'text', + 'gallery' + ]; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $inputTypes, $response); } /** @@ -213,11 +225,17 @@ public function testUnDefinedAttributeType() * @param array $attributeTypes * @param array $expectedAttributeCodes * @param array $entityTypes + * @param array $inputTypes * @param array $actualResponse * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private function assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityTypes, $actualResponse) - { + private function assertAttributeType( + $attributeTypes, + $expectedAttributeCodes, + $entityTypes, + $inputTypes, + $actualResponse + ) { $attributeMetaDataItems = array_map(null, $actualResponse['customAttributeMetadata']['items'], $attributeTypes); foreach ($attributeMetaDataItems as $itemIndex => $itemArray) { @@ -225,8 +243,9 @@ private function assertAttributeType($attributeTypes, $expectedAttributeCodes, $ $attributeMetaDataItems[$itemIndex][0], [ "attribute_code" => $expectedAttributeCodes[$itemIndex], - "attribute_type" =>$attributeTypes[$itemIndex], - "entity_type" => $entityTypes[$itemIndex] + "attribute_type" => $attributeTypes[$itemIndex], + "entity_type" => $entityTypes[$itemIndex], + "input_type" => $inputTypes[$itemIndex] ] ); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductCanonicalUrlTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductCanonicalUrlTest.php new file mode 100644 index 0000000000000..308e159b0dd77 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductCanonicalUrlTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting canonical_url for products + */ +class ProductCanonicalUrlTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store catalog/seo/product_canonical_tag 1 + * + */ + public function testProductWithCanonicalLinksMetaTagSettingsEnabled() + { + $productSku = 'simple'; + $query + = <<<QUERY +{ + products (filter: {sku: {eq: "{$productSku}"}}) { + items { + name + sku + canonical_url + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items']); + + $this->assertEquals( + 'simple-product.html', + $response['products']['items'][0]['canonical_url'] + ); + $this->assertEquals('simple', $response['products']['items'][0]['sku']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store catalog/seo/product_canonical_tag 0 + */ + public function testProductWithCanonicalLinksMetaTagSettingsDisabled() + { + $productSku = 'simple'; + $query + = <<<QUERY +{ + products (filter: {sku: {eq: "{$productSku}"}}) { + items { + name + sku + canonical_url + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertNull( + $response['products']['items'][0]['canonical_url'] + ); + $this->assertEquals('simple', $response['products']['items'][0]['sku']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php index b957292a3ac28..52463485a34f9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php @@ -144,6 +144,6 @@ private function checkImageExists(string $url): bool curl_exec($connection); $responseStatus = curl_getinfo($connection, CURLINFO_HTTP_CODE); - return $responseStatus === 200 ? true : false; + return $responseStatus === 200; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php new file mode 100644 index 0000000000000..af237f1bd6fb5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -0,0 +1,1047 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Api\LinkManagementInterface; +use Magento\ConfigurableProduct\Model\LinkManagement; +use Magento\Customer\Model\Group; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductPriceTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** @var ProductRepositoryInterface $productRepository */ + private $productRepository; + + protected function setUp() :void + { + $this->objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + */ + public function testProductWithSinglePrice() + { + $skus = ['simple']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ]; + + $this->assertPrices($expectedPriceRange, $product['price_range']); + } + + /** + * Pricing for Simple, Grouped and Configurable products with no special or tier prices configured + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_12345.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php + */ + public function testMultipleProductTypes() + { + $skus = ["simple-1", "12345", "grouped"]; + + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(3, $result['products']['items']); + + $expected = [ + "simple-1" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ], + "12345" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 30 + ], + "final_price" => [ + "value" => 30 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 40 + ], + "final_price" => [ + "value" => 40 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ], + "grouped" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 100 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 100 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ] + ]; + + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertPrices($expected[$product['sku']], $product['price_range']); + } + } + + /** + * Simple products with special price and tier price with % discount + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSimpleProductsWithSpecialPriceAndTierPrice() + { + $skus = ["simple1", "simple2"]; + $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); + + /** @var $tierPriceExtensionAttributesFactory */ + $tierPriceExtensionAttributesFactory = $this->objectManager->create(ProductTierPriceExtensionFactory::class); + $tierPriceExtensionAttribute = $tierPriceExtensionAttributesFactory->create()->setPercentageValue(10); + + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2 + ] + ] + )->setExtensionAttributes($tierPriceExtensionAttribute); + foreach ($skus as $sku) { + /** @var Product $simpleProduct */ + $simpleProduct = $this->productRepository->get($sku); + $simpleProduct->setTierPrices($tierPrices); + $this->productRepository->save($simpleProduct); + } + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(2, $result['products']['items']); + + $expectedPriceRange = [ + "simple1" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 40.1 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 40.1 + ] + ] + ], + "simple2" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 15.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 20.05 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 15.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 20.05 + ] + ] + ] + ]; + $expectedTierPrices = [ + "simple1" => [ + 0 => [ + 'discount' =>[ + 'amount_off' => 1, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] + ], + "simple2" => [ + 0 => [ + 'discount' =>[ + 'amount_off' => 2, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 18], + 'quantity' => 2 + ] + + ] + ]; + + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + $this->assertPrices($expectedPriceRange[$product['sku']], $product['price_range']); + $this->assertResponseFields($product['price_tiers'], $expectedTierPrices[$product['sku']]); + } + } + + /** + * Check the pricing for a grouped product with simple products having special price set + * + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + */ + public function testGroupedProductsWithSpecialPriceAndTierPrices() + { + $groupedProductSku = 'grouped'; + $grouped = $this->productRepository->get($groupedProductSku); + //get the associated products + $groupedProductLinks = $grouped->getProductLinks(); + $tierPriceData = [ + [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 87 + ] + ]; + $associatedProductSkus = []; + foreach ($groupedProductLinks as $groupedProductLink) { + $associatedProductSkus[] = $groupedProductLink->getLinkedProductSku(); + } + + foreach ($associatedProductSkus as $associatedProductSku) { + $associatedProduct = $this->productRepository->get($associatedProductSku); + $associatedProduct->setSpecialPrice('95.75'); + $this->productRepository->save($associatedProduct); + $this->saveProductTierPrices($associatedProduct, $tierPriceData); + } + $skus = ['grouped']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 95.75 + ], + "discount" => [ + "amount_off" => 100 - 95.75, + //difference between original and final over original price + "percent_off" => (100 - 95.75)*100/100 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 95.75 + ], + "discount" => [ + "amount_off" => 100 - 95.75, + "percent_off" => (100 - 95.75)*100/100 + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertEmpty($product['price_tiers']); + + // update default quantity of each of the associated products to be greater than tier price qty of each of them + foreach ($groupedProductLinks as $groupedProductLink) { + $groupedProductLink->getExtensionAttributes()->setQty(3); + } + $this->productRepository->save($grouped); + $result = $this->graphQlQuery($query); + $product = $result['products']['items'][0]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertEmpty($product['price_tiers']); + } + + /** + * Check pricing for bundled product with one item having special price set and dynamic price turned off + * + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_1.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testBundledProductWithSpecialPriceAndTierPrice() + { + $bundledProductSku = 'bundle-product'; + /** @var Product $bundled */ + $bundled = $this->productRepository->get($bundledProductSku); + $skus = ['bundle-product']; + $bundled->setSpecialPrice(10); + + // set the tier price for the bundled product + $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); + /** @var $tierPriceExtensionAttributesFactory */ + $tierPriceExtensionAttributesFactory = $this->objectManager->create(ProductTierPriceExtensionFactory::class); + $tierPriceExtensionAttribute = $tierPriceExtensionAttributesFactory->create()->setPercentageValue(10); + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2 + ] + ] + )->setExtensionAttributes($tierPriceExtensionAttribute); + $bundled->setTierPrices($tierPrices); + // Set Price view to PRICE RANGE + $bundled->setPriceView(0); + $this->productRepository->save($bundled); + + //Bundled product with dynamic prices turned OFF + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + + $bundleRegularPrice = 10; + $firstOptionPrice = 2.75; + $secondOptionPrice = 6.75; + + $minRegularPrice = $bundleRegularPrice + $firstOptionPrice ; + //Apply special price of 10% on minRegular price + $minFinalPrice = round($minRegularPrice * 0.1, 2); + + $maxRegularPrice = $bundleRegularPrice + $secondOptionPrice; + $maxFinalPrice = round($maxRegularPrice* 0.1, 2); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => $minRegularPrice + ], + "final_price" => [ + "value" => $minFinalPrice + ], + "discount" => [ + "amount_off" => $minRegularPrice - $minFinalPrice, + "percent_off" => round(($minRegularPrice - $minFinalPrice)*100/$minRegularPrice, 2) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $maxRegularPrice + ], + "final_price" => [ + "value" => $maxFinalPrice + ], + "discount" => [ + "amount_off" => $maxRegularPrice - $maxFinalPrice, + "percent_off" => round(($maxRegularPrice - $maxFinalPrice)*100/$maxRegularPrice, 2) + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertResponseFields( + $product['price_tiers'], + [ + 0 => [ + 'discount' =>[ + 'amount_off' => 1, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] + ] + ); + } + + /** + * Check pricing for bundled product with spl price, tier price with dynamic price turned on + * + * @magentoApiDataFixture Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php + */ + public function testBundledWithSpecialPriceAndTierPriceWithDynamicPrice() + { + $skus = ['bundle-product']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + + $minRegularPrice = 10; + $maxRegularPrice = 20; + + //Apply 10% special price on the cheapest simple product in bundle + $minFinalPrice = round(5.99 * 0.1, 2); + //Apply 10% special price on the expensive product in bundle + $maxFinalPrice = round(15.99 * 0.1, 2); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => $minRegularPrice + ], + "final_price" => [ + "value" => $minFinalPrice + ], + "discount" => [ + "amount_off" => $minRegularPrice - $minFinalPrice, + "percent_off" => round(($minRegularPrice - $minFinalPrice)*100/$minRegularPrice, 2) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $maxRegularPrice + ], + "final_price" => [ + "value" => $maxFinalPrice + ], + "discount" => [ + "amount_off" => $maxRegularPrice - $maxFinalPrice, + "percent_off" => round(($maxRegularPrice - $maxFinalPrice)*100/$maxRegularPrice, 2) + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertResponseFields( + $product['price_tiers'], + [ + 0 => [ + 'discount' =>[ + 'amount_off' => 1, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 0], + 'quantity' => 2 + ] + ] + ); + } + + /** + * Check pricing for Configurable product with each variants having special price and tier prices + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_12345.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConfigurableProductWithVariantsHavingSpecialAndTierPrices() + { + $configurableProductSku ='12345'; + /** @var LinkManagementInterface $configurableProductLink */ + $configurableProductLinks = $this->objectManager->get(LinkManagement::class); + $configurableProductVariants = $configurableProductLinks->getChildren($configurableProductSku); + $tierPriceData = [ + [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 20 + ] + ]; + foreach ($configurableProductVariants as $configurableProductVariant) { + $configurableProductVariant->setSpecialPrice('25.99'); + $this->productRepository->save($configurableProductVariant); + $this->saveProductTierPrices($configurableProductVariant, $tierPriceData); + } + $sku = ['12345']; + $query = $this->getQueryConfigurableProductAndVariants($sku); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $regularPrice = []; + $finalPrice = []; + foreach ($configurableProductVariants as $configurableProductVariant) { + $regularPrice[] = $configurableProductVariant->getPrice(); + $finalPrice[] = $configurableProductVariant->getSpecialPrice(); + } + $regularPriceCheapestVariant = 30; + $specialPrice = 25.99; + $regularPriceExpensiveVariant = 40; + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => $regularPriceCheapestVariant + ], + "final_price" => [ + "value" => $specialPrice + ], + "discount" => [ + "amount_off" => $regularPriceCheapestVariant - $specialPrice, + "percent_off" => round( + ($regularPriceCheapestVariant - $specialPrice)*100/$regularPriceCheapestVariant, + 2 + ) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $regularPriceExpensiveVariant + ], + "final_price" => [ + "value" => $specialPrice + ], + "discount" => [ + "amount_off" => $regularPriceExpensiveVariant - $specialPrice, + "percent_off" => round( + ($regularPriceExpensiveVariant - $specialPrice)*100/$regularPriceExpensiveVariant, + 2 + ) + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + //configurable product's tier price is empty + $this->assertEmpty($product['price_tiers']); + $this->assertCount(2, $product['variants']); + + $configurableVariantsInResponse = array_map(null, $product['variants'], $configurableProductVariants); + + foreach ($configurableVariantsInResponse as $key => $configurableVariantPriceData) { + //validate that the tier prices and price range for each configurable variants are not empty + $this->assertNotEmpty($configurableVariantPriceData[0]['product']['price_range']); + $this->assertNotEmpty($configurableVariantPriceData[0]['product']['price_tiers']); + $this->assertResponseFields( + $configurableVariantsInResponse[$key][0]['product']['price_range'], + [ + "minimum_price" => [ + "regular_price" => [ + "value" => $configurableProductVariants[$key]->getPrice() + ], + "final_price" => [ + "value" => round($configurableProductVariants[$key]->getSpecialPrice(), 2) + ], + "discount" => [ + "amount_off" => ($regularPrice[$key] - $finalPrice[$key]), + "percent_off" => round(($regularPrice[$key] - $finalPrice[$key])*100/$regularPrice[$key], 2) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $configurableProductVariants[$key]->getPrice() + ], + "final_price" => [ + "value" => round($configurableProductVariants[$key]->getSpecialPrice(), 2) + ], + "discount" => [ + "amount_off" => $regularPrice[$key] - $finalPrice[$key], + "percent_off" => round(($regularPrice[$key] - $finalPrice[$key])*100/$regularPrice[$key], 2) + ] + ] + ] + ); + + $this->assertResponseFields( + $configurableVariantsInResponse[$key][0]['product']['price_tiers'], + [ + 0 => [ + 'discount' =>[ + 'amount_off' => $regularPrice[$key] - $tierPriceData[0]['value'], + 'percent_off' => round( + ( + $regularPrice[$key] - $tierPriceData[0]['value'] + ) * 100/$regularPrice[$key], + 2 + ) + ], + 'final_price' =>['value'=> $tierPriceData[0]['value']], + 'quantity' => 2 + ] + ] + ); + } + } + + /** + * Check the pricing for downloadable product type + * + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + */ + public function testDownloadableProductWithSpecialPriceAndTierPrices() + { + $downloadableProductSku = 'downloadable-product'; + /** @var Product $downloadableProduct */ + $downloadableProduct = $this->productRepository->get($downloadableProductSku); + //setting the special price for the product + $downloadableProduct->setSpecialPrice('5.75'); + $this->productRepository->save($downloadableProduct); + //setting the tier price data for the product + $tierPriceData = [ + [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 7 + ] + ]; + $this->saveProductTierPrices($downloadableProduct, $tierPriceData); + $skus = ['downloadable-product']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.75 + ], + "discount" => [ + "amount_off" => 4.25, + //discount amount over regular price value + "percent_off" => (4.25/10)*100 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.75 + ], + "discount" => [ + "amount_off" => 4.25, + "percent_off" => (4.25/10)*100 + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertResponseFields( + $product['price_tiers'], + [ + 0 => [ + 'discount' =>[ + //regualr price - tier price value + 'amount_off' => 3, + 'percent_off' => 30 + ], + 'final_price' =>['value'=> 7], + 'quantity' => 2 + ] + ] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/CatalogRule/_files/catalog_rule_10_off_not_logged.php + */ + public function testProductWithCatalogDiscount() + { + $skus = ["virtual-product", "configurable"]; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(2, $result['products']['items']); + + $expected = [ + "virtual-product" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 9 + ], + "discount" => [ + "amount_off" => 1, + "percent_off" => 10 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 9 + ], + "discount" => [ + "amount_off" => 1, + "percent_off" => 10 + ] + ] + ], + "configurable" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 9 + ], + "discount" => [ + "amount_off" => 1, + "percent_off" => 10 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 18 + ], + "discount" => [ + "amount_off" => 2, + "percent_off" => 10 + ] + ] + ] + ]; + + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertPrices($expected[$product['sku']], $product['price_range']); + } + } + + /** + * Get GraphQl query to fetch products by sku + * + * @param array $skus + * @return string + */ + private function getProductQuery(array $skus): string + { + $stringSkus = '"' . implode('","', $skus) . '"'; + return <<<QUERY +{ + products(filter: {sku: {in: [$stringSkus]}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + } + price_tiers{ + discount{ + amount_off + percent_off + } + final_price{ + value + } + quantity + } + } + } +} +QUERY; + } + + /** + * Get GraphQl query to fetch Configurable product and its variants by sku + * + * @param array $sku + * @return string + */ + private function getQueryConfigurableProductAndVariants(array $sku): string + { + $stringSku = '"' . implode('","', $sku) . '"'; + return <<<QUERY +{ + products(filter: {sku: {in: [$stringSku]}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price {regular_price + { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + } + price_tiers{ + discount{ + amount_off + percent_off + } + final_price{value} + quantity + } + ... on ConfigurableProduct{ + variants{ + product{ + + sku + price_range { + minimum_price {regular_price {value} + final_price { + value + + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + + } + final_price { + value + + } + discount { + amount_off + percent_off + } + } + } + price_tiers{ + discount{ + amount_off + percent_off + } + final_price{value} + quantity + } + + } + } + } + } + } + } + +QUERY; + } + + /** + * Check prices from graphql response + * + * @param $expectedPrices + * @param $actualPrices + * @param string $currency + */ + private function assertPrices($expectedPrices, $actualPrices, $currency = 'USD') + { + $priceTypes = ['minimum_price', 'maximum_price']; + + foreach ($priceTypes as $priceType) { + $expected = $expectedPrices[$priceType]; + $actual = $actualPrices[$priceType]; + $this->assertEquals($expected['regular_price']['value'], $actual['regular_price']['value']); + $this->assertEquals( + $expected['regular_price']['currency'] ?? $currency, + $actual['regular_price']['currency'] + ); + $this->assertEquals($expected['final_price']['value'], $actual['final_price']['value']); + $this->assertEquals( + $expected['final_price']['currency'] ?? $currency, + $actual['final_price']['currency'] + ); + $this->assertEquals($expected['discount']['amount_off'], $actual['discount']['amount_off']); + $this->assertEquals($expected['discount']['percent_off'], $actual['discount']['percent_off']); + } + } + + /** + * @param ProductInterface $product + * @param array $tierPriceData + */ + private function saveProductTierPrices(ProductInterface $product, array $tierPriceData) + { + $tierPrices =[]; + $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); + foreach ($tierPriceData as $tierPrice) { + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => $tierPrice + ] + ); + /** ProductInterface $product */ + $product->setTierPrices($tierPrices); + $product->save(); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php new file mode 100644 index 0000000000000..f647dc74ea55f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductSearchAggregationsTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_with_boolean_attribute.php + */ + public function testAggregationBooleanAttribute() + { + $this->reindex(); + + $skus= '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"'; + $query = <<<QUERY +{ + products(filter: {sku: {in: [{$skus}]}}){ + items{ + id + sku + name + } + aggregations{ + label + attribute_code + count + options{ + label + value + count + } + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('items', $result['products']); + $this->assertCount(5, $result['products']['items']); + $this->assertArrayHasKey('aggregations', $result['products']); + + $booleanAggregation = array_filter( + $result['products']['aggregations'], + function ($a) { + return $a['attribute_code'] == 'boolean_attribute'; + } + ); + $this->assertNotEmpty($booleanAggregation); + $booleanAggregation = reset($booleanAggregation); + $this->assertEquals('Boolean Attribute', $booleanAggregation['label']); + $this->assertEquals('boolean_attribute', $booleanAggregation['attribute_code']); + $this->assertContains(['label' => '1', 'value'=> '1', 'count' => '3'], $booleanAggregation['options']); + + $this->markTestIncomplete('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); + $this->assertEquals(2, $booleanAggregation['count']); + $this->assertCount(2, $booleanAggregation['options']); + $this->assertContains(['label' => '0', 'value'=> '0', 'count' => '2'], $booleanAggregation['options']); + } + + /** + * Reindex + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function reindex() + { + $appDir = dirname(Bootstrap::getInstance()->getAppTempDir()); + // phpcs:ignore Magento2.Security.InsecureFunction + exec("php -f {$appDir}/bin/magento indexer:reindex"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 4ce8ad8dab393..9ee3b3baa5fc2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -13,34 +13,72 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; -use Magento\Framework\EntityManager\MetadataPool; +use Magento\Eav\Model\Config; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Model\Product; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\DataObject; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CacheCleaner; /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessiveClassLength) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ class ProductSearchTest extends GraphQlAbstract { /** - * Verify that layered navigation filters are returned for product query + * Verify that filters for non-existing category are empty * + * @throws \Exception + */ + public function testFilterForNonExistingCategory() + { + $query = <<<QUERY +{ + products(filter: {category_id: {eq: "99999999"}}) { + filters { + name + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey( + 'filters', + $response['products'], + 'Filters are missing in product query result.' + ); + + $this->assertEmpty( + $response['products']['filters'], + 'Returned filters data set does not empty' + ); + } + + /** + * Verify that layered navigation filters and aggregations are correct for product query + * + * Filter products by an array of skus + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterLn() { + $this->reIndexAndCleanCache(); $query = <<<QUERY { products ( filter: { sku: { - like:"simple%" + in:["simple1", "simple2"] } } pageSize: 4 @@ -72,9 +110,6 @@ public function testFilterLn() } } QUERY; - /** - * @var ProductRepositoryInterface $productRepository - */ $response = $this->graphQlQuery($query); $this->assertArrayHasKey( @@ -82,13 +117,853 @@ public function testFilterLn() $response['products'], 'Filters are missing in product query result.' ); + + $expectedFilters = $this->getExpectedFiltersDataSet(); + $actualFilters = $response['products']['filters']; + // presort expected and actual results as different search engines have different orders + usort($expectedFilters, [$this, 'compareFilterNames']); + usort($actualFilters, [$this, 'compareFilterNames']); + $this->assertFilters( - $response, - $this->getExpectedFiltersDataSet(), + ['products' => ['filters' => $actualFilters]], + $expectedFilters, 'Returned filters data set does not match the expected value' ); } + /** + * Compare arrays by value in 'name' field. + * + * @param array $a + * @param array $b + * @return int + */ + private function compareFilterNames(array $a, array $b) + { + return strcmp($a['name'], $b['name']); + } + + /** + * Layered navigation for Configurable products with out of stock options + * Two configurable products each having two variations and one of the child products of one Configurable set to OOS + * + * @magentoApiDataFixture Magento/Catalog/_files/category.php + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testLayeredNavigationForConfigurableProducts() + { + CacheCleaner::cleanAll(); + $attributeCode = 'test_configurable'; + + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $firstOption = $options[0]->getValue(); + $secondOption = $options[1]->getValue(); + $query = $this->getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption); + $this->reIndexAndCleanCache(); + $response = $this->graphQlQuery($query); + + $this->assertEquals(2, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['aggregations']); + $this->assertNotEmpty($response['products']['filters'], 'Filters is empty'); + $this->assertCount(2, $response['products']['aggregations'], 'Aggregation count does not match'); + + // Custom attribute filter layer data + $this->assertResponseFields( + $response['products']['aggregations'][1], + [ + 'attribute_code' => $attribute->getAttributeCode(), + 'label'=> $attribute->getDefaultFrontendLabel(), + 'count'=> 2, + 'options' => [ + [ + 'label' => 'Option 1', + 'value' => $firstOption, + 'count' =>'2' + ], + [ + 'label' => 'Option 2', + 'value' => $secondOption, + 'count' =>'2' + ] + ], + ] + ); + } + + /** + * + * @return string + */ + private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $firstOption, $secondOption) : string + { + return <<<QUERY +{ + products(filter:{ + $attributeCode: {in:["{$firstOption}", "{$secondOption}"]} + } + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + } + aggregations{ + attribute_code + count + label + options{ + label + value + count + } + } + + } +} +QUERY; + } + + /** + * Filter products by custom attribute of dropdown type and filterTypeInput eq + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterProductsByDropDownCustomAttribute() + { + CacheCleaner::cleanAll(); + $attributeCode = 'second_test_configurable'; + $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); + $query = <<<QUERY +{ + products(filter:{ + $attributeCode: {eq: "{$optionValue}"} + } + + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + + } + aggregations{ + attribute_code + count + label + options + { + label + count + value + } + } + + } +} +QUERY; + + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('12345'); + $product3 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2, $product3 ]; + $countOfFilteredProducts = count($filteredProducts); + $this->reIndexAndCleanCache(); + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count'], 'Number of products returned is incorrect'); + $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); + $this->assertCount(3, $response['products']['aggregations'], 'Incorrect count of aggregations'); + + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + for ($itemIndex = 0; $itemIndex < $countOfFilteredProducts; $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = $objectManager->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', 'second_test_configurable'); + // Validate custom attribute filter layer data from aggregations + $this->assertResponseFields( + $response['products']['aggregations'][2], + [ + 'attribute_code' => $attribute->getAttributeCode(), + 'count'=> 1, + 'label'=> $attribute->getDefaultFrontendLabel(), + 'options' => [ + [ + 'label' => 'Option 3', + 'count' => 3, + 'value' => $optionValue + ], + ], + ] + ); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function reIndexAndCleanCache() : void + { + $appDir = dirname(Bootstrap::getInstance()->getAppTempDir()); + $out = ''; + // phpcs:ignore Magento2.Security.InsecureFunction + exec("php -f {$appDir}/bin/magento indexer:reindex", $out); + CacheCleaner::cleanAll(); + } + + /** + * Filter products using an array of multi select custom attributes + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterProductsByMultiSelectCustomAttributes() + { + $objectManager = Bootstrap::getObjectManager(); + $this->reIndexAndCleanCache(); + $attributeCode = 'multiselect_attribute'; + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $countOptions = count($options); + $optionValues = []; + for ($i = 0; $i < $countOptions; $i++) { + $optionValues[] = $options[$i]->getValue(); + } + $query = <<<QUERY +{ + products(filter:{ + $attributeCode: {in:["{$optionValues[0]}", "{$optionValues[1]}", "{$optionValues[2]}"]} + } + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + } + aggregations{ + attribute_code + count + label + options + { + label + value + + } + } + + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['filters']); + $this->assertNotEmpty($response['products']['aggregations']); + } + + /** + * Get the option value for the custom attribute to be used in the graphql query + * + * @param string $attributeCode + * @return string + */ + private function getDefaultAttributeOptionValue(string $attributeCode) : string + { + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $defaultOptionValue = $options[0]->getValue(); + return $defaultOptionValue; + } + + /** + * Full text search for Products and then filter the results by custom attribute ( sort is by defaulty by relevance) + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSearchAndFilterByCustomAttribute() + { + $this->reIndexAndCleanCache(); + $attribute_code = 'second_test_configurable'; + $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); + + $query = <<<QUERY +{ + products(search:"Simple", + filter:{ + $attribute_code: {in:["{$optionValue}"]} + } + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + + } + aggregations + { + attribute_code + count + label + options + { + count + label + value + } + } + + } + +} +QUERY; + $response = $this->graphQlQuery($query); + //Verify total count of the products returned + $this->assertEquals(3, $response['products']['total_count']); + $this->assertArrayHasKey('filters', $response['products']); + $this->assertCount(3, $response['products']['aggregations']); + $expectedFilterLayers = + [ + ['name' => 'Category', + 'request_var'=> 'cat' + ], + ['name' => 'Second Test Configurable', + 'request_var'=> 'second_test_configurable' + ] + ]; + $layers = array_map(null, $expectedFilterLayers, $response['products']['filters']); + + //Verify all the three layers from filters : Price, Category and Custom attribute layers + foreach ($layers as $layerIndex => $layerFilterData) { + $this->assertNotEmpty($layerFilterData); + $this->assertEquals( + $layers[$layerIndex][0]['name'], + $response['products']['filters'][$layerIndex]['name'], + 'Layer name does not match' + ); + $this->assertEquals( + $layers[$layerIndex][0]['request_var'], + $response['products']['filters'][$layerIndex]['request_var'], + 'request_var does not match' + ); + } + + // Validate the price layer of aggregations from the response + $this->assertResponseFields( + $response['products']['aggregations'][0], + [ + 'attribute_code' => 'price', + 'count'=> 2, + 'label'=> 'Price', + 'options' => [ + [ + 'count' => 2, + 'label' => '10-20', + 'value' => '10_20', + + ], + [ + 'count' => 1, + 'label' => '40-*', + 'value' => '40_*', + + ], + ], + ] + ); + // Validate the custom attribute layer of aggregations from the response + $this->assertResponseFields( + $response['products']['aggregations'][2], + [ + 'attribute_code' => $attribute_code, + 'count'=> 1, + 'label'=> 'Second Test Configurable', + 'options' => [ + [ + 'count' => 3, + 'label' => 'Option 3', + 'value' => $optionValue, + + ] + + ], + ] + ); + // 7 categories including the subcategories to which the items belong to , are returned + $this->assertCount(7, $response['products']['aggregations'][1]['options']); + unset($response['products']['aggregations'][1]['options']); + $this->assertResponseFields( + $response['products']['aggregations'][1], + [ + 'attribute_code' => 'category_id', + 'count'=> 7, + 'label'=> 'Category' + ] + ); + } + + /** + * Filter by category and custom attribute + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterByCategoryIdAndCustomAttribute() + { + $this->reIndexAndCleanCache(); + $categoryId = 13; + $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + $query = <<<QUERY +{ + products(filter:{ + category_id : {eq:"{$categoryId}"} + second_test_configurable: {eq: "{$optionValue}"} + }, + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + } + aggregations + { + attribute_code + count + label + options + { + count + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(2, $response['products']['total_count']); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2]; + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku() + ] + ); + } + $this->assertNotEmpty($response['products']['filters'], 'filters is empty'); + $this->assertNotEmpty($response['products']['aggregations'], 'Aggregations should not be empty'); + $this->assertCount(3, $response['products']['aggregations']); + + $actualCategoriesFromResponse = $response['products']['aggregations'][1]['options']; + + //Validate the number of categories/sub-categories that contain the products with the custom attribute + $this->assertCount(6, $actualCategoriesFromResponse); + + $expectedCategoryInAggregrations = + [ + [ + 'count' => 2, + 'label' => 'Category 1', + 'value'=> '3' + ], + [ + 'count'=> 1, + 'label' => 'Category 1.1', + 'value'=> '4' + + ], + [ + 'count'=> 1, + 'label' => 'Movable Position 2', + 'value'=> '10' + + ], + [ + 'count'=> 1, + 'label' => 'Movable Position 3', + 'value'=> '11' + ], + [ + 'count'=> 1, + 'label' => 'Category 12', + 'value'=> '12' + + ], + [ + 'count'=> 2, + 'label' => 'Category 1.2', + 'value'=> '13' + ], + ]; + // presort expected and actual results as different search engines have different orders + usort($expectedCategoryInAggregrations, [$this, 'compareLabels']); + usort($actualCategoriesFromResponse, [$this, 'compareLabels']); + $categoryInAggregations = array_map(null, $expectedCategoryInAggregrations, $actualCategoriesFromResponse); + + //Validate the categories and sub-categories data in the filter layer + foreach ($categoryInAggregations as $index => $categoryAggregationsData) { + $this->assertNotEmpty($categoryAggregationsData); + $this->assertEquals( + $categoryInAggregations[$index][0]['label'], + $actualCategoriesFromResponse[$index]['label'], + 'Category is incorrect' + ); + $this->assertEquals( + $categoryInAggregations[$index][0]['count'], + $actualCategoriesFromResponse[$index]['count'], + 'Products count in the category is incorrect' + ); + } + } + + /** + * Compare arrays by value in 'label' field. + * + * @param array $a + * @param array $b + * @return int + */ + private function compareLabels(array $a, array $b) + { + return strcmp($a['label'], $b['label']); + } + + /** + * Filter by exact match of product url key + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterBySingleProductUrlKey() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get('simple-4'); + $urlKey = $product->getUrlKey(); + + $query = <<<QUERY +{ + products(filter:{ + url_key:{eq:"{$urlKey}"} + } + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + url_key + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + } + aggregations + { + attribute_code + count + label + options + { + count + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(1, $response['products']['total_count'], 'More than 1 product found'); + $this->assertCount(2, $response['products']['aggregations']); + $this->assertResponseFields( + $response['products']['items'][0], + [ + 'name' => $product->getName(), + 'sku' => $product->getSku(), + 'url_key'=> $product->getUrlKey() + ] + ); + $this->assertEquals('Price', $response['products']['aggregations'][0]['label']); + $this->assertEquals('Category', $response['products']['aggregations'][1]['label']); + //Disable the product + $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED); + $productRepository->save($product); + $query2 = <<<QUERY +{ + products(filter:{ + url_key:{eq:"{$urlKey}"} + } + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + url_key + } + + filters{ + name + request_var + filter_items_count + } + aggregations + { + attribute_code + count + label + options + { + count + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query2); + $this->assertEquals(0, $response['products']['total_count'], 'Total count should be zero'); + $this->assertEmpty($response['products']['items']); + $this->assertEmpty($response['products']['aggregations']); + } + + /** + * Filter by multiple product url keys + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testFilterByMultipleProductUrlKeys() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product1 = $productRepository->get('simple'); + $product2 = $productRepository->get('12345'); + $product3 = $productRepository->get('simple-4'); + $filteredProducts = [$product1, $product2, $product3]; + $urlKey =[]; + foreach ($filteredProducts as $product) { + $urlKey[] = $product->getUrlKey(); + } + + $query = <<<QUERY +{ + products(filter:{ + url_key:{in:["{$urlKey[0]}", "{$urlKey[1]}", "{$urlKey[2]}"]} + } + pageSize: 3 + currentPage: 1 + ) + { + total_count + items + { + name + sku + url_key + } + page_info{ + current_page + page_size + + } + filters{ + name + request_var + filter_items_count + } + aggregations + { + attribute_code + count + label + options + { + count + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count'], 'Total count is incorrect'); + $this->assertCount(2, $response['products']['aggregations']); + + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $this->assertNotEmpty($productItemsInResponse[$itemIndex]); + //validate that correct products are returned + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ 'name' => $filteredProducts[$itemIndex]->getName(), + 'sku' => $filteredProducts[$itemIndex]->getSku(), + 'url_key'=> $filteredProducts[$itemIndex]->getUrlKey() + ] + ); + } + } + /** * Get array with expected data for layered navigation filters * @@ -157,8 +1032,8 @@ private function getExpectedFiltersDataSet() private function assertFilters($response, $expectedFilters, $message = '') { $this->assertArrayHasKey('filters', $response['products'], 'Product has filters'); - $this->assertTrue(is_array(($response['products']['filters'])), 'Product filters is array'); - $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty'); + $this->assertTrue(is_array(($response['products']['filters'])), 'Product filters is not array'); + $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is empty'); foreach ($expectedFilters as $expectedFilter) { $found = false; foreach ($response['products']['filters'] as $responseFilter) { @@ -175,12 +1050,13 @@ private function assertFilters($response, $expectedFilters, $message = '') } /** - * Verify that items between the price range of 5 and 50 are returned after sorting name in DESC + * Verify product filtering using price range AND matching skus AND name sorted in DESC order * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() + public function testFilterWithinSpecificPriceRangeSortedByNameDesc() { $query = <<<QUERY @@ -188,12 +1064,9 @@ public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() products( filter: { - price:{gt: "5", lt: "50"} - or: - { - sku:{like:"simple%"} - name:{like:"Simple%"} - } + price:{from: "5", to: "50"} + sku:{in:["simple1", "simple2"]} + name:{match:"Simple"} } pageSize:4 currentPage:1 @@ -245,90 +1118,19 @@ public function testFilterProductsWithinSpecificPriceRangeSortedByNameDesc() $this->assertEquals(4, $response['products']['page_info']['page_size']); } - /** - * Test a visible product with matching sku or name with special price - * - * Requesting for items that has a special price and price < $60, that are visible in Catalog, Search or Both which - * either has a sku like “simple” or name like “configurable”sorted by price in DESC - * - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testFilterVisibleProductsWithMatchingSkuOrNameWithSpecialPrice() - { - $query - = <<<QUERY -{ - products( - filter: - { - special_price:{neq:"null"} - price:{lt:"60"} - or: - { - sku:{like:"%simple%"} - name:{like:"%configurable%"} - } - weight:{eq:"1"} - } - pageSize:6 - currentPage:1 - sort: - { - price:DESC - } - ) - { - items - { - sku - price { - minimalPrice { - amount { - value - currency - } - } - } - name - ... on PhysicalProductInterface { - weight - } - type_id - attribute_set_id - } - total_count - page_info - { - page_size - current_page - } - } -} -QUERY; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $product1 = $productRepository->get('simple1'); - $product2 = $productRepository->get('simple2'); - $filteredProducts = [$product2, $product1]; - - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('total_count', $response['products']); - $this->assertEquals(2, $response['products']['total_count']); - $this->assertProductItems($filteredProducts, $response); - } - /** * pageSize = total_count and current page = 2 * expected - error is thrown * Actual - empty array * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSearchWithFilterWithPageSizeEqualTotalCount() { + $this->reIndexAndCleanCache(); $query = <<<QUERY { @@ -336,14 +1138,7 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() search : "simple" filter: { - special_price:{neq:"null"} - price:{lt:"60"} - or: - { - sku:{like:"%simple%"} - name:{like:"%configurable%"} - } - weight:{eq:"1"} + price:{from:"5.59"} } pageSize:2 currentPage:2 @@ -389,12 +1184,13 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() } /** - * Requesting for items that match a specific SKU or NAME within a certain price range sorted by Price in ASC order + * Filtering for products and sorting using multiple sort parameters * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQueryProductsInCurrentPageSortedByPriceASC() + public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() { $query = <<<QUERY @@ -402,18 +1198,17 @@ public function testQueryProductsInCurrentPageSortedByPriceASC() products( filter: { - price:{gt: "5", lt: "50"} - or: - { - sku:{like:"simple%"} - name:{like:"simple%"} - } + price:{to :"50"} + sku:{in:["simple1", "simple2"]} + name:{match:"Simple"} + } pageSize:4 currentPage:1 sort: { price:ASC + name:ASC } ) { @@ -478,22 +1273,25 @@ public function testQueryProductsInCurrentPageSortedByPriceASC() } /** - * Verify the items is correct after sorting their name in ASC order + * Filtering products by fuzzy name match * - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php + * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php */ - public function testQueryProductsSortedByNameASC() + public function testFilterProductsForExactMatchingName() { + $query = <<<QUERY { products( filter: { - sku:{in:["simple2", "simple1"]} + name: { + match:"shorts" + } } - pageSize:1 - currentPage:2 + pageSize:2 + currentPage:1 sort: { name:ASC @@ -510,6 +1308,16 @@ public function testQueryProductsSortedByNameASC() { page_size current_page + } + aggregations{ + attribute_code + count + label + options{ + label + value + count + } } } } @@ -518,72 +1326,98 @@ public function testQueryProductsSortedByNameASC() * @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple2'); - + $product1 = $productRepository->get('grey_shorts'); + $product2 = $productRepository->get('white_shorts'); $response = $this->graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - $this->assertEquals(['page_size' => 1, 'current_page' => 2], $response['products']['page_info']); + $this->assertEquals(['page_size' => 2, 'current_page' => 1], $response['products']['page_info']); $this->assertEquals( - [['sku' => $product->getSku(), 'name' => $product->getName()]], + [ + ['sku' => $product1->getSku(), 'name' => $product1->getName()], + ['sku' => $product2->getSku(), 'name' => $product2->getName()] + ], $response['products']['items'] ); + $this->assertArrayHasKey('aggregations', $response['products']); + $this->assertCount(2, $response['products']['aggregations']); + $expectedAggregations =[ + [ + 'attribute_code' => 'price', + 'count' => 2, + 'label' => 'Price', + 'options' => [ + [ + 'label' => '10-20', + 'value' => '10_20', + 'count' => 1, + ], + [ + 'label' => '20-*', + 'value' => '20_*', + 'count' => 1, + ] + ] + ], + [ + 'attribute_code' => 'category_id', + 'count' => 1, + 'label' => 'Category', + 'options' => [ + [ + 'label' => 'Colorful Category', + 'value' => '330', + 'count' => 2, + ], + ], + ] + ]; + $this->assertEquals($expectedAggregations, $response['products']['aggregations']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php + * @magentoApiDataFixture Magento/Catalog/_files/categories.php */ - public function testFilteringForProductInMultipleCategories() + public function testFilteringForProductsFromMultipleCategories() { - $productSku = 'simple333'; $query = <<<QUERY { - products(filter:{sku:{eq:"{$productSku}"}}) + products(filter:{ + category_id :{in:["4","5","12"]} + }) { - items{ - id - sku - name - attribute_set_id - categories { - id + items + { + sku + name + } + total_count + filters{ + request_var + name + filter_items_count + filter_items{ + value_string + label + } + } } - } - } } QUERY; $response = $this->graphQlQuery($query); /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var ProductInterface $product */ - $product = $productRepository->get('simple333'); - $categoryIds = $product->getCategoryIds(); - foreach ($categoryIds as $index => $value) { - $categoryIds[$index] = [ 'id' => (int)$value]; - } - $this->assertNotEmpty($response['products']['items'][0]['categories'], "Categories must not be empty"); - $this->assertNotNull($response['products']['items'][0]['categories'], "categories must not be null"); - $this->assertEquals($categoryIds, $response['products']['items'][0]['categories']); - /** @var MetadataPool $metaData */ - $metaData = ObjectManager::getInstance()->get(MetadataPool::class); - $linkField = $metaData->getMetadata(ProductInterface::class)->getLinkField(); - $assertionMap = [ - - ['response_field' => 'id', 'expected_value' => $product->getData($linkField)], - ['response_field' => 'sku', 'expected_value' => $product->getSku()], - ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()] - ]; - $this->assertResponseFields($response['products']['items'][0], $assertionMap); + $this->assertEquals(3, $response['products']['total_count']); } /** + * Filter products by single category + * * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php * @return void */ - public function testFilterProductsByCategoryIds() + public function testFilterProductsBySingleCategoryId() { $queryCategoryId = 333; $query @@ -619,7 +1453,7 @@ public function testFilterProductsByCategoryIds() QUERY; $response = $this->graphQlQuery($query); - + $this->assertEquals(2, $response['products']['total_count'], 'Incorrect count of products returned'); /** @var CategoryLinkManagement $productLinks */ $productLinks = ObjectManager::getInstance()->get(CategoryLinkManagement::class); /** @var CategoryRepositoryInterface $categoryRepository */ @@ -663,12 +1497,90 @@ public function testFilterProductsByCategoryIds() } /** - * Sorting by price in the DESC order from the filtered items with default pageSize + * Sorting the search results by relevance (DESC => most relevant) + * + * Search for products for a fuzzy match and checks if all matching results returned including + * results based on matching keywords from description + * + * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php + * @return void + */ + public function testSearchAndSortByRelevance() + { + $this->reIndexAndCleanCache(); + $search_term ="blue"; + $query + = <<<QUERY +{ + products( + search:"{$search_term}" + sort:{relevance:DESC} + pageSize: 5 + currentPage: 1 + ) + { + total_count + items + { + name + sku + } + page_info{ + current_page + page_size + total_pages + } + filters{ + name + request_var + filter_items_count + filter_items{ + label + items_count + value_string + __typename + } + } + aggregations{ + attribute_code + count + label + options{ + label + value + count + } + } + } + +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(3, $response['products']['total_count']); + $this->assertNotEmpty($response['products']['filters'], 'Filters should have the Category layer'); + $this->assertEquals('Colorful Category', $response['products']['filters'][0]['filter_items'][0]['label']); + $this->assertCount(2, $response['products']['aggregations']); + $productsInResponse = ['Blue briefs','Navy Blue Striped Shoes','Grey shorts']; + /** @var \Magento\Config\Model\Config $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Config\Model\Config::class); + if (strpos($config->getConfigDataValue('catalog/search/engine'), 'elasticsearch') !== false) { + $this->markTestIncomplete('MC-20716'); + } + $count = count($response['products']['items']); + for ($i = 0; $i < $count; $i++) { + $this->assertEquals($productsInResponse[$i], $response['products']['items'][$i]['name']); + } + } + + /** + * Filtering for product with sku "equals" a specific value + * If pageSize and current page are not requested, default values are returned * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testQuerySortByPriceDESCWithDefaultPageSize() + public function testFilterByExactSkuAndSortByPriceDesc() { $query = <<<QUERY @@ -676,16 +1588,10 @@ public function testQuerySortByPriceDESCWithDefaultPageSize() products( filter: { - price:{gt: "5", lt: "60"} - or: - { - sku:{like:"%simple%"} - name:{like:"%Configurable%"} - } + sku:{eq:"simple1"} } sort: { - price:DESC } ) @@ -720,83 +1626,35 @@ public function testQuerySortByPriceDESCWithDefaultPageSize() /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $visibleProduct1 = $productRepository->get('simple1'); - $visibleProduct2 = $productRepository->get('simple2'); - $filteredProducts = [$visibleProduct2, $visibleProduct1]; + + $filteredProducts = [$visibleProduct1]; $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(1, $response['products']['total_count']); $this->assertProductItems($filteredProducts, $response); $this->assertEquals(20, $response['products']['page_info']['page_size']); $this->assertEquals(1, $response['products']['page_info']['current_page']); } - - /** - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - */ - public function testProductQueryUsingFromAndToFilterInput() - { - $query - = <<<QUERY -{ - products( - filter: { - price:{ - from:"5" to:"20" - } - } - sort: { - sku: DESC - } - ) { - total_count - items { - attribute_set_id - sku - name - price { - minimalPrice { - amount { - value - currency - } - } - maximalPrice { - amount { - value - currency - } - } - } - type_id - ...on PhysicalProductInterface { - weight - } - } - } -} -QUERY; - - $response = $this->graphQlQuery($query); - $this->assertEquals(2, $response['products']['total_count']); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $product1 = $productRepository->get('simple1'); - $product2 = $productRepository->get('simple2'); - $filteredProducts = [$product2, $product1]; - - $this->assertProductItemsWithMaximalAndMinimalPriceCheck($filteredProducts, $response); - } - /** - * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php + * Fuzzy search filtered for price and sorted by price and name + * + * @magentoApiDataFixture Magento/Catalog/_files/products_for_relevance_sorting.php */ public function testProductBasicFullTextSearchQuery() { - $textToSearch = 'Simple'; + $this->reIndexAndCleanCache(); + $textToSearch = 'blue'; $query =<<<QUERY { products( search: "{$textToSearch}" + filter:{ + price:{to:"50"} + } + sort:{ + price:DESC + name:ASC + } ) { total_count @@ -816,18 +1674,36 @@ public function testProductBasicFullTextSearchQuery() page_size current_page } + filters{ + filter_items { + items_count + label + value_string + } + } + aggregations{ + attribute_code + count + label + options{ + count + label + value + } + } } } QUERY; /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $prod1 = $productRepository->get('simple1'); - + $prod1 = $productRepository->get('blue_briefs'); + $prod2 = $productRepository->get('grey_shorts'); + $prod3 = $productRepository->get('navy-striped-shoes'); $response = $this->graphQlQuery($query); - $this->assertEquals(1, $response['products']['total_count']); + $this->assertEquals(3, $response['products']['total_count']); - $filteredProducts = [$prod1]; + $filteredProducts = [$prod1, $prod2, $prod3]; $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); foreach ($productItemsInResponse as $itemIndex => $itemArray) { $this->assertNotEmpty($itemArray); @@ -839,7 +1715,7 @@ public function testProductBasicFullTextSearchQuery() 'price' => [ 'minimalPrice' => [ 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getSpecialPrice(), + 'value' => $filteredProducts[$itemIndex]->getPrice(), 'currency' => 'USD' ] ] @@ -850,24 +1726,43 @@ public function testProductBasicFullTextSearchQuery() } /** + * Filter products purely in a given price range + * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php */ - public function testProductsThatMatchWithPricesFromList() + public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $filteredProducts = [$prod1, $prod2]; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + foreach ($filteredProducts as $product) { + $categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [333] + ); + } + $query =<<<QUERY - { +{ products( filter: { - price:{in:["10","20"]} + price:{from:"5" to: "20"} } pageSize:4 currentPage:1 sort: { - name:DESC + price:ASC } ) { @@ -876,6 +1771,18 @@ public function testProductsThatMatchWithPricesFromList() attribute_set_id sku price { + minimalPrice { + amount { + value + currency + } + } + maximalPrice { + amount { + value + currency + } + } regularPrice { amount { value @@ -890,6 +1797,12 @@ public function testProductsThatMatchWithPricesFromList() type_id } total_count + filters + { + request_var + name + filter_items_count + } page_info { page_size @@ -898,40 +1811,23 @@ public function testProductsThatMatchWithPricesFromList() } } QUERY; + $response = $this->graphQlQuery($query); $this->assertEquals(2, $response['products']['total_count']); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - - $prod1 = $productRepository->get('simple2'); - $prod2 = $productRepository->get('simple1'); - $filteredProducts = [$prod1, $prod2]; - $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); - foreach ($productItemsInResponse as $itemIndex => $itemArray) { - $this->assertNotEmpty($itemArray); - $this->assertResponseFields( - $productItemsInResponse[$itemIndex][0], - ['attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), - 'sku' => $filteredProducts[$itemIndex]->getSku(), - 'name' => $filteredProducts[$itemIndex]->getName(), - 'price' => [ - 'regularPrice' => [ - 'amount' => [ - 'value' => $filteredProducts[$itemIndex]->getPrice(), - 'currency' => 'USD' - ] - ] - ], - 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), - 'weight' => $filteredProducts[$itemIndex]->getWeight() - ] - ); + $this->assertProductItemsWithPriceCheck($filteredProducts, $response); + //verify that by default Price and category are the only layers available + $filterNames = ['Category', 'Price']; + $this->assertCount(2, $response['products']['filters'], 'Filter count does not match'); + $productCount = count($response['products']['filters']); + for ($i = 0; $i < $productCount; $i++) { + $this->assertEquals($filterNames[$i], $response['products']['filters'][$i]['name']); } } /** * No items are returned if the conditions are not met * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -942,21 +1838,17 @@ public function testQueryFilterNoMatchingItems() { products( filter: - { - special_price:{lt:"15"} - price:{lt:"50"} - weight:{gt:"4"} - or: - { - sku:{like:"simple%"} - name:{like:"%simple%"} - } + { + price:{from:"50"} + + description:{match:"Description"} + } pageSize:2 currentPage:1 sort: { - sku:ASC + position:ASC } ) { @@ -995,6 +1887,7 @@ public function testQueryFilterNoMatchingItems() /** * Asserts that exception is thrown when current page > totalCount of items returned * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1006,7 +1899,7 @@ public function testQueryPageOutOfBoundException() products( filter: { - price:{eq:"10"} + price:{to:"10"} } pageSize:2 currentPage:2 @@ -1053,6 +1946,7 @@ public function testQueryPageOutOfBoundException() } /** + * No filter or search arguments used * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testQueryWithNoSearchOrFilterArgumentException() @@ -1098,7 +1992,7 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() products( filter: { - sku:{like:"simple%"} + sku:{eq:"simple_visible_in_stock"} } pageSize:20 @@ -1140,6 +2034,7 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() /** * Verify that invalid current page return an error * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @expectedException \Exception * @expectedExceptionMessage currentPage value must be greater than 0 @@ -1151,7 +2046,7 @@ public function testInvalidCurrentPage() products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 4 @@ -1169,6 +2064,7 @@ public function testInvalidCurrentPage() /** * Verify that invalid page size returns an error. * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @expectedException \Exception * @expectedExceptionMessage pageSize value must be greater than 0 @@ -1180,7 +2076,7 @@ public function testInvalidPageSize() products ( filter: { sku: { - like:"simple%" + eq:"simple2" } } pageSize: 0 @@ -1204,8 +2100,8 @@ public function testInvalidPageSize() private function assertProductItems(array $filteredProducts, array $actualResponse) { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); - // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall - for ($itemIndex = 0; $itemIndex < count($filteredProducts); $itemIndex++) { + $count = count($filteredProducts); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { $this->assertNotEmpty($productItemsInResponse[$itemIndex]); $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], @@ -1227,7 +2123,7 @@ private function assertProductItems(array $filteredProducts, array $actualRespon } } - private function assertProductItemsWithMaximalAndMinimalPriceCheck(array $filteredProducts, array $actualResponse) + private function assertProductItemsWithPriceCheck(array $filteredProducts, array $actualResponse) { $productItemsInResponse = array_map(null, $actualResponse['products']['items'], $filteredProducts); @@ -1250,7 +2146,14 @@ private function assertProductItemsWithMaximalAndMinimalPriceCheck(array $filter 'value' => $filteredProducts[$itemIndex]->getSpecialPrice(), 'currency' => 'USD' ] - ] + ], + 'regularPrice' => [ + 'amount' => [ + 'value' => $filteredProducts[$itemIndex]->getPrice(), + 'currency' => 'USD' + ] + ] + ], 'type_id' =>$filteredProducts[$itemIndex]->getTypeId(), 'weight' => $filteredProducts[$itemIndex]->getWeight() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index e11e2e8d108c2..3ade1a0ef17d0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -282,7 +282,6 @@ public function testQueryAllFieldsSimpleProduct() $this->assertBaseFields($product, $response['products']['items'][0]); $this->assertEavAttributes($product, $response['products']['items'][0]); $this->assertOptions($product, $response['products']['items'][0]); - $this->assertTierPrices($product, $response['products']['items'][0]); $this->assertArrayHasKey('websites', $response['products']['items'][0]); $this->assertWebsites($product, $response['products']['items'][0]['websites']); self::assertEquals( @@ -293,11 +292,8 @@ public function testQueryAllFieldsSimpleProduct() 'Filter category', $responseObject->getData('products/items/0/categories/1/name') ); - $storeManager = ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManagerInterface::class); - self::assertEquals( - $storeManager->getStore()->getBaseUrl() . 'simple-product.html', - $responseObject->getData('products/items/0/canonical_url') - ); + //canonical_url will be null unless the admin setting catalog/seo/product_canonical_tag is turned ON + self::assertNull($responseObject->getData('products/items/0/canonical_url')); } /** @@ -592,7 +588,7 @@ public function testProductPrices() $secondProductSku = 'simple-156'; $query = <<<QUERY { - products(filter: {min_price: {gt: "100.0"}, max_price: {gt: "150.0", lt: "250.0"}}) + products(filter: {price: {from: "150.0", to: "250.0"}}) { items { attribute_set_id @@ -723,24 +719,7 @@ private function assertCustomAttribute($actualResponse) $customAttribute = null; $this->assertEquals($customAttribute, $actualResponse['attribute_code_custom']); } - - /** - * @param ProductInterface $product - * @param $actualResponse - */ - private function assertTierPrices($product, $actualResponse) - { - $tierPrices = $product->getTierPrices(); - $this->assertNotEmpty($actualResponse['tier_prices'], "Precondition failed: 'tier_prices' must not be empty"); - foreach ($actualResponse['tier_prices'] as $tierPriceIndex => $tierPriceArray) { - foreach ($tierPriceArray as $key => $value) { - /** @var \Magento\Catalog\Model\Product\TierPrice $tierPrice */ - $tierPrice = $tierPrices[$tierPriceIndex]; - $this->assertEquals($value, $tierPrice->getData($key)); - } - } - } - + /** * @param ProductInterface $product * @param $actualResponse @@ -795,6 +774,7 @@ private function assertOptions($product, $actualResponse) ]; $this->assertResponseFields($value, $assertionMapValues); } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $assertionMap = array_merge( $assertionMap, [ @@ -823,7 +803,7 @@ private function assertOptions($product, $actualResponse) $valueKeyName = 'date_option'; $valueAssertionMap = []; } - + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $valueAssertionMap = array_merge( $valueAssertionMap, [ @@ -980,7 +960,7 @@ public function testProductInAllAnchoredCategories() { $query = <<<QUERY { - products(filter: {sku: {like: "12345%"}}) + products(filter: {sku: {in: ["12345"]}}) { items { @@ -1030,7 +1010,7 @@ public function testProductWithNonAnchoredParentCategory() { $query = <<<QUERY { - products(filter: {sku: {like: "12345%"}}) + products(filter: {sku: {in: ["12345"]}}) { items { @@ -1084,7 +1064,11 @@ public function testProductInNonAnchoredSubCategories() { $query = <<<QUERY { - products(filter: {sku: {like: "12345%"}}) + products(filter: + { + sku: {in:["12345"]} + } + ) { items { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php index 21e608da4800a..0982007daaa44 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php @@ -40,7 +40,8 @@ public function testGetStoreConfig() list_per_page_values, grid_per_page, list_per_page, - catalog_default_sort_by + catalog_default_sort_by, + root_category_id } } QUERY; @@ -56,5 +57,52 @@ public function testGetStoreConfig() $this->assertEquals('8', $response['storeConfig']['list_per_page_values']); $this->assertEquals(8, $response['storeConfig']['list_per_page']); $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); + $this->assertEquals(2, $response['storeConfig']['root_category_id']); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture catalog/seo/product_url_suffix global_test_product_suffix + * @magentoConfigFixture catalog/seo/category_url_suffix global_test_category_suffix + * @magentoConfigFixture catalog/seo/title_separator __ + * @magentoConfigFixture catalog/frontend/list_mode 3 + * @magentoConfigFixture catalog/frontend/grid_per_page_values 16 + * @magentoConfigFixture catalog/frontend/list_per_page_values 8 + * @magentoConfigFixture catalog/frontend/grid_per_page 16 + * @magentoConfigFixture catalog/frontend/list_per_page 8 + * @magentoConfigFixture catalog/frontend/default_sort_by asc + */ + public function testGetStoreConfigGlobal() + { + $query + = <<<QUERY +{ + storeConfig{ + product_url_suffix, + category_url_suffix, + title_separator, + list_mode, + grid_per_page_values, + list_per_page_values, + grid_per_page, + list_per_page, + catalog_default_sort_by, + root_category_id + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('storeConfig', $response); + + $this->assertEquals('global_test_product_suffix', $response['storeConfig']['product_url_suffix']); + $this->assertEquals('global_test_category_suffix', $response['storeConfig']['category_url_suffix']); + $this->assertEquals('__', $response['storeConfig']['title_separator']); + $this->assertEquals('3', $response['storeConfig']['list_mode']); + $this->assertEquals('16', $response['storeConfig']['grid_per_page_values']); + $this->assertEquals(16, $response['storeConfig']['grid_per_page']); + $this->assertEquals('8', $response['storeConfig']['list_per_page_values']); + $this->assertEquals(8, $response['storeConfig']['list_per_page']); + $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); + $this->assertEquals(2, $response['storeConfig']['root_category_id']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php index 43796d780646c..0d808c6dd0696 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php @@ -12,6 +12,8 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDTO; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Store\Model\StoreManagerInterface; /** * Test of getting URL rewrites data from products @@ -54,9 +56,22 @@ public function testProductWithNoCategoriesAssigned() $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $product = $productRepository->get('virtual-product', false, null, true); + $storeId = ObjectManager::getInstance()->get(StoreManagerInterface::class)->getStore()->getId(); $urlFinder = ObjectManager::getInstance()->get(UrlFinderInterface::class); + $entityType = ObjectManager::getInstance()->create(EavConfig::class)->getEntityType('catalog_product'); - $rewritesCollection = $urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $product->getId()]); + $entityTypeCode = $entityType->getEntityTypeCode(); + if ($entityTypeCode === 'catalog_product') { + $entityTypeCode = 'product'; + } + + $rewritesCollection = $urlFinder->findAllByData( + [ + UrlRewriteDTO::ENTITY_ID => $product->getId(), + UrlRewriteDTO::ENTITY_TYPE => $entityTypeCode, + UrlRewriteDTO::STORE_ID => $storeId + ] + ); /* There should be only one rewrite */ /** @var UrlRewriteDTO $urlRewrite */ @@ -110,18 +125,32 @@ public function testProductWithOneCategoryAssigned() $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $product = $productRepository->get('simple', false, null, true); + $storeId = ObjectManager::getInstance()->get(StoreManagerInterface::class)->getStore()->getId(); $urlFinder = ObjectManager::getInstance()->get(UrlFinderInterface::class); + $entityType = ObjectManager::getInstance()->create(EavConfig::class)->getEntityType('catalog_product'); - $rewritesCollection = $urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $product->getId()]); - $rewritesCount = count($rewritesCollection); + $entityTypeCode = $entityType->getEntityTypeCode(); + if ($entityTypeCode === 'catalog_product') { + $entityTypeCode = 'product'; + } + $rewritesCollection = $urlFinder->findAllByData( + [ + UrlRewriteDTO::ENTITY_ID => $product->getId(), + UrlRewriteDTO::ENTITY_TYPE => $entityTypeCode, + UrlRewriteDTO::STORE_ID => $storeId + ] + ); + + $rewritesCount = count($rewritesCollection); $this->assertArrayHasKey('url_rewrites', $response['products']['items'][0]); + $this->assertCount(1, $response['products']['items'][0]['url_rewrites']); $this->assertCount($rewritesCount, $response['products']['items'][0]['url_rewrites']); - for ($i = 0; $i < $rewritesCount; $i++) { - $urlRewrite = $rewritesCollection[$i]; + for ($index = 0; $index < $rewritesCount; $index++) { + $urlRewrite = $rewritesCollection[$index]; $this->assertResponseFields( - $response['products']['items'][0]['url_rewrites'][$i], + $response['products']['items'][0]['url_rewrites'][$index], [ "url" => $urlRewrite->getRequestPath(), "parameters" => $this->getUrlParameters($urlRewrite->getTargetPath()) @@ -140,11 +169,12 @@ private function getUrlParameters(string $targetPath): array { $urlParameters = []; $targetPathParts = explode('/', trim($targetPath, '/')); - - for ($i = 3; ($i < sizeof($targetPathParts) - 1); $i += 2) { + $count = count($targetPathParts) - 1; + //phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ($index = 3; $index < $count; $index += 2) { $urlParameters[] = [ - 'name' => $targetPathParts[$i], - 'value' => $targetPathParts[$i + 1] + 'name' => $targetPathParts[$index], + 'value' => $targetPathParts[$index + 1] ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php new file mode 100644 index 0000000000000..52985422c3355 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCms/CategoryBlockTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCms; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Widget\Model\Template\FilterEmulate; + +/** + * Test category cms fields are resolved correctly + */ +class CategoryBlockTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/category_tree.php + * @magentoApiDataFixture Magento/Cms/_files/block.php + */ + public function testCategoryCmsBlock() + { + $blockId = 'fixture_block'; + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = Bootstrap::getObjectManager()->get(BlockRepositoryInterface::class); + $block = $blockRepository->getById($blockId); + $filter = Bootstrap::getObjectManager()->get(FilterEmulate::class); + $renderedContent = $filter->setUseSessionInUrl(false)->filter($block->getContent()); + + /** @var CategoryRepositoryInterface $categoryRepository */ + $categoryRepository = Bootstrap::getObjectManager()->get(CategoryRepositoryInterface::class); + $category = $categoryRepository->get(401); + $category->setLandingPage($block->getId()); + $categoryRepository->save($category); + + $query = <<<QUERY +{ + category(id: 401){ + name + cms_block{ + identifier + title + content + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $response); + $this->assertNotEmpty($response['category']); + $actualBlock = $response['category']['cms_block']; + + $this->assertEquals($block->getTitle(), $actualBlock['title']); + $this->assertEquals($block->getIdentifier(), $actualBlock['identifier']); + $this->assertEquals($renderedContent, $actualBlock['content']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForCustomersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForCustomersTest.php new file mode 100644 index 0000000000000..95f012f798d02 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForCustomersTest.php @@ -0,0 +1,332 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +class TierPricesForCustomersTest extends GraphQlAbstract +{ + /** @var \Magento\TestFramework\ObjectManager */ + private $objectManager; + + /** @var GetMaskedQuoteIdByReservedOrderId */ + private $getMaskedQuoteIdByReservedOrderId; + + /** @var CustomerTokenServiceInterface */ + private $customerTokenService; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForGeneralGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData =[ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForGeneralAndAllCustomerGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 6 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 7 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 6 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 7 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(2, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForNotLoggedInGroupOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertEmpty($response['products']['items'][0]['tier_prices']); + + $expectedResponse = []; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForNotLoggedInAndGeneralGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 7, + 'value'=> 6.5 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForAllCustomerGroupsOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ], + + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForAllCustomerGroupsAndNotLoggedInGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ], + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6.5 + ], + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @param ProductInterface $product + * @param array $tierPriceData + */ + private function saveTierPrices($product, $tierPriceData) + { + $tierPrices = []; + /** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ + $tierPriceFactory = $this->objectManager + ->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + foreach ($tierPriceData as $tierPrice) { + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => $tierPrice + ] + ); + } + $product->setTierPrices($tierPrices); + $product->save(); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + sku + name + tier_prices { + customer_group_id + percentage_value + qty + value + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + $headerMap = [ 'Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForGuestsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForGuestsTest.php new file mode 100644 index 0000000000000..d4c834c0aea6a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForGuestsTest.php @@ -0,0 +1,301 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; + +class TierPricesForGuestsTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForGeneralGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData =[ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + + $expectedResponse = []; + $this->assertEmpty($response['products']['items'][0]['tier_prices']); + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForGeneralAndAllCustomerGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 6 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 6 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForNotLoggedInGroupOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForNotLoggedInAndGeneralGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 6.5 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 5, + 'value'=> 6 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 6.5 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForAllCustomerGroupsOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ], + + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForAllCustomerGroupsAndNotLoggedInGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ], + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6.5 + ], + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ], + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6.5 + ] + ]; + + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(2, $response['products']['items'][0]['tier_prices']); + } + + /** + * @param ProductInterface $product + * @param array $tierPriceData + */ + private function saveTierPrices($product, $tierPriceData) + { + $tierPrices = []; + /** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ + $tierPriceFactory = $this->objectManager + ->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + foreach ($tierPriceData as $tierPrice) { + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => $tierPrice + ] + ); + } + $product->setTierPrices($tierPrices); + $product->save(); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + sku + name + tier_prices { + customer_group_id + percentage_value + qty + value + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php new file mode 100644 index 0000000000000..29696e29908fe --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php @@ -0,0 +1,499 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogUrlRewrite; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Model\UrlRewrite; + +/** + * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. + */ +class UrlResolverTest extends GraphQlAbstract +{ + /** @var ObjectManager */ + private $objectManager; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * Tests if target_path(relative_url) is resolved for Product entity + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlResolver() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + $redirectCode = $actualUrls->getRedirectType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $urlPath, + $relativePath, + $expectedType, + $redirectCode + ); + } + + /** + * Test the use case where non seo friendly is provided as resolver input in the Query + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlWithNonSeoFriendlyUrlInput() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + // even of non seo friendly path requested, the seo friendly path should be prefered + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + $nonSeoFriendlyPath = $actualUrls->getTargetPath(); + $redirectCode = $actualUrls->getRedirectType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $nonSeoFriendlyPath, + $relativePath, + $expectedType, + $redirectCode + ); + } + + /** + * Test the use case where non seo friendly is provided as resolver input in the Query + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testRedirectsAndCustomInput() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + // generate permanent redirects + $renamedKey = 'p002-ren'; + $product->setUrlKey($renamedKey)->setData('save_rewrites_history', true)->save(); + + $storeId = $product->getStoreId(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + $suffix = $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + // querying the end redirect gives the same record + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $renamedKey . $suffix, + $actualUrls->getRequestPath(), + $actualUrls->getEntityType(), + 0 + ); + + // querying a url that's a redirect the active redirected final url + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $productSku . $suffix, + $actualUrls->getRequestPath(), + $actualUrls->getEntityType(), + 301 + ); + + // create custom url that doesn't redirect + /** @var UrlRewrite $urlRewriteModel */ + $urlRewriteModel = $this->objectManager->create(UrlRewrite::class); + + $customUrl = 'custom-path'; + $urlRewriteArray = [ + 'entity_type' => 'custom', + 'entity_id' => '0', + 'request_path' => $customUrl, + 'target_path' => 'p002.html', + 'redirect_type' => '0', + 'store_id' => '1', + 'description' => '', + 'is_autogenerated' => '0', + 'metadata' => null, + ]; + foreach ($urlRewriteArray as $key => $value) { + $urlRewriteModel->setData($key, $value); + } + $urlRewriteModel->save(); + + // querying a custom url that should return the target entity but relative should be the custom url + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $customUrl, + $customUrl, + $actualUrls->getEntityType(), + 0 + ); + + // change custom url that does redirect + $urlRewriteModel->setRedirectType('301'); + $urlRewriteModel->setId($urlRewriteModel->getId()); + $urlRewriteModel->save(); + + ObjectManager::getInstance()->get(\Magento\TestFramework\Helper\CacheCleaner::class)->cleanAll(); + + //modifying query by adding spaces to avoid getting cached values. + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $customUrl, + $actualUrls->getRequestPath(), + strtoupper($actualUrls->getEntityType()), + 301 + ); + $urlRewriteModel->delete(); + } + + /** + * Test for category entity + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCategoryUrlResolver() + { + $productSku = 'p002'; + $categoryUrlPath = 'cat-1.html'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlPath, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + + $query + = <<<QUERY +{ + category(id:{$categoryId}) { + url_key + url_suffix + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['category']['url_key'] . $response['category']['url_suffix']; + + $this->queryUrlAndAssertResponse( + (int) $categoryId, + $urlPath, + $relativePath, + $expectedType, + 0 + ); + } + + /** + * Test the use case where the url_key of the existing product is changed + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlRewriteResolver() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + $product->setUrlKey('p002-new')->save(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + + $this->assertEquals($urlPath, 'p002-new' . $response['products']['items'][0]['url_suffix']); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $urlPath, + $relativePath, + $expectedType, + 0 + ); + } + + /** + * Tests if null is returned when an invalid request_path is provided as input to urlResolver + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testInvalidUrlResolverInput() + { + $productSku = 'p002'; + $urlPath = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + $query + = <<<QUERY +{ + urlResolver(url:"{$urlPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertNull($response['urlResolver']); + } + + /** + * Test for category entity with leading slash + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCategoryUrlWithLeadingSlash() + { + $productSku = 'p002'; + $categoryUrlPath = 'cat-1.html'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlPath, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + + $query + = <<<QUERY +{ + category(id:{$categoryId}) { + url_key + url_suffix + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['category']['url_key'] . $response['category']['url_suffix']; + $urlPathWithLeadingSlash = "/{$urlPath}"; + $this->queryUrlAndAssertResponse( + (int) $categoryId, + $urlPathWithLeadingSlash, + $relativePath, + $expectedType, + 0 + ); + } + + /** + * Test for custom type which point to the valid product/category/cms page. + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testGetNonExistentUrlRewrite() + { + $urlPath = 'non-exist-product.html'; + /** @var UrlRewrite $urlRewrite */ + $urlRewrite = $this->objectManager->create(UrlRewrite::class); + $urlRewrite->load($urlPath, 'request_path'); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => 1 + ] + ); + $relativePath = $actualUrls->getRequestPath(); + + $query = <<<QUERY +{ + urlResolver(url:"{$urlPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals('PRODUCT', $response['urlResolver']['type']); + $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * Assert response from GraphQl + * + * @param string $productId + * @param string $urlKey + * @param string $relativePath + * @param string $expectedType + * @param int $redirectCode + */ + private function queryUrlAndAssertResponse( + int $productId, + string $urlKey, + string $relativePath, + string $expectedType, + int $redirectCode + ): void { + $query + = <<<QUERY +{ + urlResolver(url:"{$urlKey}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($productId, $response['urlResolver']['id']); + $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $this->assertEquals($redirectCode, $response['urlResolver']['redirectCode']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php new file mode 100644 index 0000000000000..072c6bc38de70 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CmsUrlRewrite; + +use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Cms\Helper\Page as PageHelper; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. + */ +class UrlResolverTest extends GraphQlAbstract +{ + /** @var ObjectManager */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testCMSPageUrlResolver() + { + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load('page100'); + $cmsPageId = $page->getId(); + $requestPath = $page->getIdentifier(); + + /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ + $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); + + /** @param \Magento\Cms\Api\Data\PageInterface $page */ + $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); + $expectedEntityType = CmsPageUrlRewriteGenerator::ENTITY_TYPE; + + $query + = <<<QUERY +{ + urlResolver(url:"{$requestPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + + // querying by non seo friendly url path should return seo friendly relative url + $query + = <<<QUERY +{ + urlResolver(url:"{$targetPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * Test resolution of '/' path to home page + */ + public function testResolveSlash() + { + /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface */ + $scopeConfigInterface = $this->objectManager->get(ScopeConfigInterface::class); + $homePageIdentifier = $scopeConfigInterface->getValue( + PageHelper::XML_PATH_HOME_PAGE, + ScopeInterface::SCOPE_STORE + ); + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load($homePageIdentifier); + $homePageId = $page->getId(); + $query + = <<<QUERY +{ + urlResolver(url:"/") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($homePageId, $response['urlResolver']['id']); + $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); + $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php index 378d3e7dcd9aa..b1858e843bf0f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\ConfigurableProduct; +use Exception; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -139,6 +140,62 @@ public function testAddMultipleConfigurableProductToCart() } } + /** + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * + * @expectedException Exception + * @expectedExceptionMessage Could not find specified product. + */ + public function testAddVariationFromAnotherConfigurableProductWithTheSameSuperAttributeToCart() + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable_12345')); + $product = current($searchResponse['products']['items']); + + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = $product['sku']; + + $sku = 'simple_20'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $sku, + $quantity + ); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * + * @expectedException Exception + * @expectedExceptionMessage Could not find specified product. + */ + public function testAddVariationFromAnotherConfigurableProductWithDifferentSuperAttributeToCart() + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable_12345')); + $product = current($searchResponse['products']['items']); + + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = $product['sku']; + + $sku = 'simple_20'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $sku, + $quantity + ); + + $this->graphQlMutation($query); + } + /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php @@ -208,7 +265,8 @@ public function testAddNonExistentConfigurableProductVariationToCart() $this->expectException(\Exception::class); $this->expectExceptionMessage( - 'Could not add the product with SKU configurable to the shopping cart: Could not find specified product.' + 'Could not add the product with SKU configurable to the shopping cart: The product that was requested ' . + 'doesn\'t exist. Verify the product and try again.' ); $this->graphQlMutation($query); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php new file mode 100644 index 0000000000000..8f32caa9dcf0f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * checks that qty of configurable product is updated in cart + */ +class UpdateConfigurableCartItemsTest extends GraphQlAbstract +{ + /** + * @var QuoteIdMaskFactory + */ + protected $quoteIdMaskFactory; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php + */ + public function testUpdateConfigurableCartItemQuantity() + { + $reservedOrderId = 'test_cart_with_configurable'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $productSku = 'simple_10'; + $newQuantity = 123; + $quoteItem = $this->getQuoteItemBySku($productSku, $reservedOrderId); + + $query = $this->getQuery($maskedQuoteId, (int)$quoteItem->getId(), $newQuantity); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('updateCartItems', $response); + self::assertArrayHasKey('quantity', $response['updateCartItems']['cart']['items']['0']); + self::assertEquals($newQuantity, $response['updateCartItems']['cart']['items']['0']['quantity']); + } + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + } + + /** + * @param string $maskedQuoteId + * @param int $quoteItemId + * @param int $newQuantity + * @return string + */ + private function getQuery(string $maskedQuoteId, int $quoteItemId, int $newQuantity): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id:"$maskedQuoteId" + cart_items: [ + { + cart_item_id: $quoteItemId + quantity: $newQuantity + } + ] + }) { + cart { + items { + quantity + } + } + } +} +QUERY; + } + + /** + * Returns quote item by product SKU + * + * @param string $sku + * @return Item|bool + * @throws NoSuchEntityException + */ + private function getQuoteItemBySku(string $sku, string $reservedOrderId) + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $item = false; + foreach ($quote->getAllItems() as $quoteItem) { + if ($quoteItem->getSku() == $sku && $quoteItem->getProductType() == Configurable::TYPE_CODE && + !$quoteItem->getParentItemId()) { + $item = $quoteItem; + break; + } + } + + return $item; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php index f36200c8e9218..bf01ad4b37218 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php @@ -48,7 +48,7 @@ class ChangeCustomerPasswordTest extends GraphQlAbstract */ private $customerRepository; - protected function setUp() + protected function setUp(): void { $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); $this->accountManagement = Bootstrap::getObjectManager()->get(AccountManagementInterface::class); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php index 47c4d3ad91cb6..81300e967f6a2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php @@ -29,7 +29,7 @@ class CreateCustomerAddressTest extends GraphQlAbstract */ private $addressRepository; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -43,14 +43,13 @@ protected function setUp() */ public function testCreateCustomerAddress() { - $customerId = 1; $newAddress = [ 'region' => [ 'region' => 'Arizona', 'region_id' => 4, 'region_code' => 'AZ' ], - 'country_id' => 'US', + 'country_code' => 'US', 'street' => ['Line 1 Street', 'Line 2'], 'company' => 'Company name', 'telephone' => '123456789', @@ -76,7 +75,7 @@ public function testCreateCustomerAddress() region_id: {$newAddress['region']['region_id']} region_code: "{$newAddress['region']['region_code']}" } - country_id: {$newAddress['country_id']} + country_code: {$newAddress['country_code']} street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}"] company: "{$newAddress['company']}" telephone: "{$newAddress['telephone']}" @@ -99,7 +98,7 @@ public function testCreateCustomerAddress() region_id region_code } - country_id + country_code street company telephone @@ -124,15 +123,85 @@ public function testCreateCustomerAddress() $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('createCustomerAddress', $response); $this->assertArrayHasKey('customer_id', $response['createCustomerAddress']); - $this->assertEquals($customerId, $response['createCustomerAddress']['customer_id']); + $this->assertEquals(null, $response['createCustomerAddress']['customer_id']); $this->assertArrayHasKey('id', $response['createCustomerAddress']); $address = $this->addressRepository->getById($response['createCustomerAddress']['id']); $this->assertEquals($address->getId(), $response['createCustomerAddress']['id']); + $address->setCustomerId(null); $this->assertCustomerAddressesFields($address, $response['createCustomerAddress']); $this->assertCustomerAddressesFields($address, $newAddress); } + /** + * Test case for deprecated `country_id` field. + * + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreateCustomerAddressWithCountryId() + { + $newAddress = [ + 'region' => [ + 'region' => 'Arizona', + 'region_id' => 4, + 'region_code' => 'AZ' + ], + 'country_id' => 'US', + 'street' => ['Line 1 Street', 'Line 2'], + 'company' => 'Company name', + 'telephone' => '123456789', + 'fax' => '123123123', + 'postcode' => '7777', + 'city' => 'City Name', + 'firstname' => 'Adam', + 'lastname' => 'Phillis', + 'middlename' => 'A', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'vat_id' => '1', + 'default_shipping' => true, + 'default_billing' => false + ]; + + $mutation + = <<<MUTATION +mutation { + createCustomerAddress(input: { + region: { + region: "{$newAddress['region']['region']}" + region_id: {$newAddress['region']['region_id']} + region_code: "{$newAddress['region']['region_code']}" + } + country_id: {$newAddress['country_id']} + street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}"] + company: "{$newAddress['company']}" + telephone: "{$newAddress['telephone']}" + fax: "{$newAddress['fax']}" + postcode: "{$newAddress['postcode']}" + city: "{$newAddress['city']}" + firstname: "{$newAddress['firstname']}" + lastname: "{$newAddress['lastname']}" + middlename: "{$newAddress['middlename']}" + prefix: "{$newAddress['prefix']}" + suffix: "{$newAddress['suffix']}" + vat_id: "{$newAddress['vat_id']}" + default_shipping: true + default_billing: false + }) { + country_id + } +} +MUTATION; + + $userName = 'customer@example.com'; + $password = 'password'; + + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->assertArrayHasKey('createCustomerAddress', $response); + $this->assertEquals($newAddress['country_id'], $response['createCustomerAddress']['country_id']); + } + /** * @expectedException Exception * @expectedExceptionMessage The current customer isn't authorized. @@ -153,7 +222,7 @@ public function testCreateCustomerAddressIfUserIsNotAuthorized() region: { region_id: 1 } - country_id: US + country_code: US postcode: "9999" default_shipping: true default_billing: false @@ -182,7 +251,7 @@ public function testCreateCustomerAddressWithMissingAttribute() region: { region_id: 1 } - country_id: US + country_code: US street: ["Line 1 Street","Line 2"] company: "Company name" telephone: "123456789" @@ -202,6 +271,27 @@ public function testCreateCustomerAddressWithMissingAttribute() $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @expectedException Exception + * @expectedExceptionMessage "input" value should be specified + */ + public function testCreateCustomerAddressWithMissingInput() + { + $userName = 'customer@example.com'; + $password = 'password'; + $mutation = <<<MUTATION +mutation { + createCustomerAddress( + input: {} + ) { + city + } +} +MUTATION; + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -214,7 +304,7 @@ public function testCreateCustomerAddressWithRedundantStreetLine() 'region_id' => 4, 'region_code' => 'AZ' ], - 'country_id' => 'US', + 'country_code' => 'US', 'street' => ['Line 1 Street', 'Line 2', 'Line 3'], 'company' => 'Company name', 'telephone' => '123456789', @@ -240,7 +330,7 @@ public function testCreateCustomerAddressWithRedundantStreetLine() region_id: {$newAddress['region']['region_id']} region_code: "{$newAddress['region']['region_code']}" } - country_id: {$newAddress['country_id']} + country_code: {$newAddress['country_code']} street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}","{$newAddress['street'][2]}"] company: "{$newAddress['company']}" telephone: "{$newAddress['telephone']}" @@ -268,6 +358,67 @@ public function testCreateCustomerAddressWithRedundantStreetLine() $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @magentoConfigFixture default_store general/country/optional_zip_countries UA + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreateCustomerAddressWithOptionalZipCode() + { + $newAddress = [ + 'country_code' => 'UA', + 'street' => ['Line 1 Street', 'Line 2'], + 'company' => 'Company name', + 'telephone' => '123456789', + 'fax' => '123123123', + 'city' => 'City Name', + 'firstname' => 'Adam', + 'lastname' => 'Phillis', + 'middlename' => 'A', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'vat_id' => '1', + 'default_shipping' => true, + 'default_billing' => false + ]; + + $mutation + = <<<MUTATION +mutation { + createCustomerAddress(input: { + country_code: {$newAddress['country_code']} + street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}"] + company: "{$newAddress['company']}" + telephone: "{$newAddress['telephone']}" + fax: "{$newAddress['fax']}" + city: "{$newAddress['city']}" + firstname: "{$newAddress['firstname']}" + lastname: "{$newAddress['lastname']}" + middlename: "{$newAddress['middlename']}" + prefix: "{$newAddress['prefix']}" + suffix: "{$newAddress['suffix']}" + vat_id: "{$newAddress['vat_id']}" + default_shipping: true + default_billing: false + }) { + id + } +} +MUTATION; + + $userName = 'customer@example.com'; + $password = 'password'; + + $response = $this->graphQlMutation( + $mutation, + [], + '', + $this->getCustomerAuthHeaders($userName, $password) + ); + $this->assertNotEmpty($response['createCustomerAddress']['id']); + } + /** * Create new address with invalid input * @@ -303,8 +454,8 @@ public function invalidInputDataProvider() { return [ ['', 'Syntax Error: Expected Name, found )'], - ['input: ""', 'Expected type CustomerAddressInput!, found "".'], - ['input: "foo"', 'Expected type CustomerAddressInput!, found "foo".'] + ['input: ""', 'requires type CustomerAddressInput!, found "".'], + ['input: "foo"', 'requires type CustomerAddressInput!, found "foo".'] ]; } @@ -313,12 +464,15 @@ public function invalidInputDataProvider() * * @param AddressInterface $address * @param array $actualResponse + * @param string $countryFieldName */ - private function assertCustomerAddressesFields(AddressInterface $address, array $actualResponse): void - { + private function assertCustomerAddressesFields( + AddressInterface $address, + array $actualResponse + ): void { /** @var $addresses */ $assertionMap = [ - ['response_field' => 'country_id', 'expected_value' => $address->getCountryId()], + ['response_field' => 'country_code', 'expected_value' => $address->getCountryId()], ['response_field' => 'street', 'expected_value' => $address->getStreet()], ['response_field' => 'company', 'expected_value' => $address->getCompany()], ['response_field' => 'telephone', 'expected_value' => $address->getTelephone()], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php index 67c21a3798a59..a6455a9728fec 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -13,7 +13,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test for create customer functionallity + * Test for create customer functionality */ class CreateCustomerTest extends GraphQlAbstract { @@ -27,7 +27,7 @@ class CreateCustomerTest extends GraphQlAbstract */ private $customerRepository; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -68,6 +68,7 @@ public function testCreateCustomerAccountWithPassword() QUERY; $response = $this->graphQlMutation($query); + $this->assertEquals(null, $response['createCustomer']['customer']['id']); $this->assertEquals($newFirstname, $response['createCustomer']['customer']['firstname']); $this->assertEquals($newLastname, $response['createCustomer']['customer']['lastname']); $this->assertEquals($newEmail, $response['createCustomer']['customer']['email']); @@ -139,7 +140,7 @@ public function testCreateCustomerIfInputDataIsEmpty() /** * @expectedException \Exception - * @expectedExceptionMessage Required parameters are missing: Email + * @expectedExceptionMessage Required parameters are missing: Email */ public function testCreateCustomerIfEmailMissed() { @@ -251,7 +252,6 @@ public function testCreateCustomerIfNameEmpty() $newFirstname = ''; $newLastname = 'Rowe'; $currentPassword = 'test123#'; - $query = <<<QUERY mutation { createCustomer( @@ -276,7 +276,72 @@ public function testCreateCustomerIfNameEmpty() $this->graphQlMutation($query); } - public function tearDown() + /** + * @magentoConfigFixture default_store newsletter/general/active 0 + */ + public function testCreateCustomerSubscribed() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + email + is_subscribed + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + $this->assertEquals(false, $response['createCustomer']['customer']['is_subscribed']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage A customer with the same email address already exists in an associated website. + */ + public function testCreateCustomerIfCustomerWithProvidedEmailAlreadyExists() + { + $existedEmail = 'customer@example.com'; + $password = 'test123#'; + $firstname = 'John'; + $lastname = 'Smith'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + email: "{$existedEmail}" + password: "{$password}" + firstname: "{$firstname}" + lastname: "{$lastname}" + } + ) { + customer { + firstname + lastname + email + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + public function tearDown(): void { $newEmail = 'new_customer@example.com'; try { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php index 443b9d7ec53e5..31065f3f6f98b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php @@ -39,7 +39,7 @@ class DeleteCustomerAddressTest extends GraphQlAbstract */ private $lockCustomer; - protected function setUp() + protected function setUp(): void { parent::setUp(); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php index e0c6841b2ea2b..ed360919d8320 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php @@ -31,7 +31,7 @@ class GetAddressesTest extends GraphQlAbstract */ private $lockCustomer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -62,7 +62,7 @@ public function testGetCustomerWithAddresses() is_array([$response['customer']['addresses']]), " Addresses field must be of an array type." ); - self::assertEquals($customer->getId(), $response['customer']['id']); + self::assertEquals(null, $response['customer']['id']); $this->assertCustomerAddressesFields($customer, $response); } @@ -105,7 +105,7 @@ public function testGetCustomerAddressIfUserIsNotAuthorized() * @param CustomerInterface $customer * @param array $actualResponse */ - public function assertCustomerAddressesFields($customer, $actualResponse) + private function assertCustomerAddressesFields($customer, $actualResponse) { /** @var AddressInterface $addresses */ $addresses = $customer->getAddresses(); @@ -113,7 +113,7 @@ public function assertCustomerAddressesFields($customer, $actualResponse) $this->assertNotEmpty($addressValue); $assertionMap = [ ['response_field' => 'id', 'expected_value' => $addresses[$addressKey]->getId()], - ['response_field' => 'customer_id', 'expected_value' => $addresses[$addressKey]->getCustomerId()], + ['response_field' => 'customer_id', 'expected_value' => 0], ['response_field' => 'region_id', 'expected_value' => $addresses[$addressKey]->getRegionId()], ['response_field' => 'country_id', 'expected_value' => $addresses[$addressKey]->getCountryId()], ['response_field' => 'telephone', 'expected_value' => $addresses[$addressKey]->getTelephone()], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php index 928a263e8531b..c645d8953981a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\Customer; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\CustomerAuthUpdate; use Magento\Customer\Model\CustomerRegistry; use Magento\Integration\Api\CustomerTokenServiceInterface; @@ -30,13 +31,19 @@ class GetCustomerTest extends GraphQlAbstract */ private $customerAuthUpdate; - protected function setUp() + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp(): void { parent::setUp(); $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); $this->customerAuthUpdate = Bootstrap::getObjectManager()->get(CustomerAuthUpdate::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); } /** @@ -50,14 +57,21 @@ public function testGetCustomer() $query = <<<QUERY query { customer { + id firstname lastname email } } QUERY; - $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); - + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertEquals(null, $response['customer']['id']); $this->assertEquals('John', $response['customer']['firstname']); $this->assertEquals('Smith', $response['customer']['lastname']); $this->assertEquals($currentEmail, $response['customer']['email']); @@ -102,7 +116,38 @@ public function testGetCustomerIfAccountIsLocked() } } QUERY; - $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedExceptionMessage This account isn't confirmed. Verify and try again. + */ + public function testAccountIsNotConfirmed() + { + $customerEmail = 'customer@example.com'; + $currentPassword = 'password'; + $headersMap = $this->getCustomerAuthHeaders($customerEmail, $currentPassword); + $customer = $this->customerRepository->getById(1)->setConfirmation( + \Magento\Customer\Api\AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED + ); + $this->customerRepository->save($customer); + $query = <<<QUERY +query { + customer { + firstname + lastname + email + } +} +QUERY; + $this->graphQlQuery($query, [], '', $headersMap); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php index bbb7e245c91fd..4cba8323793dc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php @@ -29,7 +29,7 @@ class SubscriptionStatusTest extends GraphQlAbstract */ private $subscriberFactory; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -162,7 +162,7 @@ private function getHeaderMap(string $email, string $password): array return ['Authorization' => 'Bearer ' . $customerToken]; } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php index f2e82398df49b..da67900994940 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php @@ -40,7 +40,7 @@ class UpdateCustomerAddressTest extends GraphQlAbstract */ private $lockCustomer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -59,7 +59,6 @@ public function testUpdateCustomerAddress() { $userName = 'customer@example.com'; $password = 'password'; - $customerId = 1; $addressId = 1; $mutation = $this->getMutation($addressId); @@ -67,7 +66,7 @@ public function testUpdateCustomerAddress() $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('updateCustomerAddress', $response); $this->assertArrayHasKey('customer_id', $response['updateCustomerAddress']); - $this->assertEquals($customerId, $response['updateCustomerAddress']['customer_id']); + $this->assertEquals(null, $response['updateCustomerAddress']['customer_id']); $this->assertArrayHasKey('id', $response['updateCustomerAddress']); $address = $this->addressRepository->getById($addressId); @@ -77,6 +76,56 @@ public function testUpdateCustomerAddress() $this->assertCustomerAddressesFields($address, $updateAddress); } + /** + * Test case for deprecated `country_id` field. + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testUpdateCustomerAddressWithCountryId() + { + $userName = 'customer@example.com'; + $password = 'password'; + $addressId = 1; + + $updateAddress = $this->getAddressData(); + + $mutation = $mutation + = <<<MUTATION +mutation { + updateCustomerAddress(id: {$addressId}, input: { + region: { + region: "{$updateAddress['region']['region']}" + region_id: {$updateAddress['region']['region_id']} + region_code: "{$updateAddress['region']['region_code']}" + } + country_id: {$updateAddress['country_code']} + street: ["{$updateAddress['street'][0]}","{$updateAddress['street'][1]}"] + company: "{$updateAddress['company']}" + telephone: "{$updateAddress['telephone']}" + fax: "{$updateAddress['fax']}" + postcode: "{$updateAddress['postcode']}" + city: "{$updateAddress['city']}" + firstname: "{$updateAddress['firstname']}" + lastname: "{$updateAddress['lastname']}" + middlename: "{$updateAddress['middlename']}" + prefix: "{$updateAddress['prefix']}" + suffix: "{$updateAddress['suffix']}" + vat_id: "{$updateAddress['vat_id']}" + default_shipping: true + default_billing: true + }) { + country_id + } +} +MUTATION; + + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->assertArrayHasKey('updateCustomerAddress', $response); + $this->assertEquals($updateAddress['country_code'], $response['updateCustomerAddress']['country_id']); + } + /** * @expectedException Exception * @expectedExceptionMessage The current customer isn't authorized. @@ -127,17 +176,63 @@ public function testUpdateCustomerAddressWithMissingAttribute() $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } + /** + * Test custom attributes of the customer's address + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Customer/_files/attribute_user_defined_address_custom_attribute.php + */ + public function testUpdateCustomerAddressHasCustomAttributes() + { + $userName = 'customer@example.com'; + $password = 'password'; + $addressId = 1; + $attributes = [ + [ + 'attribute_code' => 'custom_attribute1', + 'value'=> '[new-value1,new-value2]' + ], + [ + 'attribute_code' => 'custom_attribute2', + 'value'=> '"new-value3"' + ] + ]; + $attributesFragment = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($attributes)); + $mutation + = <<<MUTATION +mutation { + updateCustomerAddress( + id: {$addressId} + input: { + custom_attributes: {$attributesFragment} + } + ) { + custom_attributes { + attribute_code + value + } + } +} +MUTATION; + + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->assertEquals($attributes, $response['updateCustomerAddress']['custom_attributes']); + } + /** * Verify the fields for Customer address * * @param AddressInterface $address * @param array $actualResponse + * @param string $countryFieldName */ - private function assertCustomerAddressesFields(AddressInterface $address, $actualResponse): void - { + private function assertCustomerAddressesFields( + AddressInterface $address, + $actualResponse + ): void { /** @var $addresses */ $assertionMap = [ - ['response_field' => 'country_id', 'expected_value' => $address->getCountryId()], + ['response_field' => 'country_code', 'expected_value' => $address->getCountryId()], ['response_field' => 'street', 'expected_value' => $address->getStreet()], ['response_field' => 'company', 'expected_value' => $address->getCompany()], ['response_field' => 'telephone', 'expected_value' => $address->getTelephone()], @@ -188,7 +283,7 @@ public function testUpdateCustomerAddressWithMissingId() region_id: {$updateAddress['region']['region_id']} region_code: "{$updateAddress['region']['region_code']}" } - country_id: {$updateAddress['country_id']} + country_code: {$updateAddress['country_code']} street: ["{$updateAddress['street'][0]}","{$updateAddress['street'][1]}"] company: "{$updateAddress['company']}" telephone: "{$updateAddress['telephone']}" @@ -244,7 +339,7 @@ public function testUpdateCustomerAddressWithInvalidIdType() region_id: {$updateAddress['region']['region_id']} region_code: "{$updateAddress['region']['region_code']}" } - country_id: {$updateAddress['country_id']} + country_code: {$updateAddress['country_code']} street: ["{$updateAddress['street'][0]}","{$updateAddress['street'][1]}"] company: "{$updateAddress['company']}" telephone: "{$updateAddress['telephone']}" @@ -306,8 +401,8 @@ public function invalidInputDataProvider() { return [ ['', '"input" value must be specified'], - ['input: ""', 'Expected type CustomerAddressInput, found ""'], - ['input: "foo"', 'Expected type CustomerAddressInput, found "foo"'] + ['input: ""', 'requires type CustomerAddressInput, found ""'], + ['input: "foo"', 'requires type CustomerAddressInput, found "foo"'] ]; } @@ -383,11 +478,11 @@ private function getAddressData(): array { return [ 'region' => [ - 'region' => 'Alaska', - 'region_id' => 2, - 'region_code' => 'AK' + 'region' => 'Alberta', + 'region_id' => 66, + 'region_code' => 'AB' ], - 'country_id' => 'US', + 'country_code' => 'CA', 'street' => ['Line 1 Street', 'Line 2'], 'company' => 'Company Name', 'telephone' => '123456789', @@ -424,7 +519,7 @@ private function getMutation(int $addressId): string region_id: {$updateAddress['region']['region_id']} region_code: "{$updateAddress['region']['region_code']}" } - country_id: {$updateAddress['country_id']} + country_code: {$updateAddress['country_code']} street: ["{$updateAddress['street'][0]}","{$updateAddress['street'][1]}"] company: "{$updateAddress['company']}" telephone: "{$updateAddress['telephone']}" @@ -447,7 +542,7 @@ private function getMutation(int $addressId): string region_id region_code } - country_id + country_code street company telephone diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index 178d10b3c35a4..7121f12bc2a42 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -33,7 +33,7 @@ class UpdateCustomerTest extends GraphQlAbstract */ private $lockCustomer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -69,7 +69,7 @@ public function testUpdateCustomer() middlename: "{$newMiddlename}" lastname: "{$newLastname}" suffix: "{$newSuffix}" - dob: "{$newDob}" + date_of_birth: "{$newDob}" taxvat: "{$newTaxVat}" email: "{$newEmail}" password: "{$currentPassword}" @@ -82,7 +82,7 @@ public function testUpdateCustomer() middlename lastname suffix - dob + date_of_birth taxvat email gender @@ -102,7 +102,7 @@ public function testUpdateCustomer() $this->assertEquals($newMiddlename, $response['updateCustomer']['customer']['middlename']); $this->assertEquals($newLastname, $response['updateCustomer']['customer']['lastname']); $this->assertEquals($newSuffix, $response['updateCustomer']['customer']['suffix']); - $this->assertEquals($newDob, $response['updateCustomer']['customer']['dob']); + $this->assertEquals($newDob, $response['updateCustomer']['customer']['date_of_birth']); $this->assertEquals($newTaxVat, $response['updateCustomer']['customer']['taxvat']); $this->assertEquals($newEmail, $response['updateCustomer']['customer']['email']); $this->assertEquals($newGender, $response['updateCustomer']['customer']['gender']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php index 6b8aad83edac7..d0ad772e9bb27 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php @@ -63,6 +63,18 @@ public function testGuestCannotAccessDownloadableProducts() { $this->graphQlQuery($this->getQuery()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_download_limit.php + * @magentoApiDataFixture Magento/Downloadable/_files/customer_order_with_downloadable_product.php + */ + public function testRemainingDownloads() + { + $query = $this->getQuery(); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('remaining_downloads', $response['customerDownloadableProducts']['items'][0]); + self::assertEquals(100, $response['customerDownloadableProducts']['items'][0]['remaining_downloads']); + } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php new file mode 100644 index 0000000000000..9fecc954d1182 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Framework; + +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; + +/** + * Test that required input parameters are properly validated on framework level + */ +class RequiredInputArgumentTest extends GraphQlAbstract +{ + + /** + * Test that a simple input value will be treated as required + * + * We should see error message from framework not the Resolver + * urlResolver query has required input arg "url" + */ + public function testSimpleInputArgumentRequired() + { + $query = <<<QUERY + { + urlResolver{ + id + type + } + } +QUERY; + + $expectedExceptionsMessage = 'GraphQL response contains errors:' + . ' Field "urlResolver" argument "url" of type "String!" is required but not provided.'; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage($expectedExceptionsMessage); + + $this->graphQlQuery($query); + } + + /** + * Test that a more complex required argument is handled properly + * + * updateCartItems mutation has required parameter input.cart_items.cart_item_id + */ + public function testInputObjectArgumentRequired() + { + $query = <<<QUERY + mutation { + updateCartItems( + input: { + cart_id: "foobar" + cart_items: [ + { + quantity: 2 + } + ] + } + ) { + cart { + total_quantity + } + } + } +QUERY; + + $expectedExceptionsMessage = 'GraphQL response contains errors:' + . ' Field CartItemUpdateInput.cart_item_id of required type Int! was not provided.'; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage($expectedExceptionsMessage); + + $this->graphQlMutation($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php index 69bcc73dd27a1..0c22bea2a42f8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php @@ -56,6 +56,124 @@ public function testIntrospectionQuery() $this->assertArrayHasKey('__schema', $this->graphQlQuery($query)); } + /** + * Tests that Introspection is allowed by default + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testIntrospectionQueryWithOnlySchema() + { + $query + = <<<QUERY + { + __schema { + queryType { name } + types{ + ...FullType + } + } + } +fragment FullType on __Type{ + name + kind + fields(includeDeprecated:true){ + name + args{ + ...InputValue + } + } + } + +fragment TypeRef on __Type { + kind + name + ofType{ + kind + name + } +} +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} +QUERY; + $this->assertArrayHasKey('__schema', $this->graphQlQuery($query)); + $response = $this->graphQlQuery($query); + + $query + = <<<QUERY +query IntrospectionQuery { + __schema { + queryType { name } + types{ + ...FullType + } + } + } +fragment FullType on __Type{ + name + kind + fields(includeDeprecated:true){ + name + args{ + ...InputValue + } + } + } + +fragment TypeRef on __Type { + kind + name + ofType{ + kind + name + } +} +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} +QUERY; + $this->assertArrayHasKey('__schema', $this->graphQlQuery($query)); + $responseFields = $this->graphQlQuery($query); + $this->assertResponseFields($response, $responseFields); + $this->assertEquals($responseFields, $response); + } + + /** + * Tests that Introspection is allowed by default + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testIntrospectionQueryWithOnlyType() + { + $query + = <<<QUERY +{ + __type(name:"Query") + { + name + kind + fields(includeDeprecated:true){ + name + type{ + kind + name + } + description + isDeprecated + deprecationReason + } + } +} +QUERY; + $this->assertArrayHasKey('__type', $this->graphQlQuery($query)); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['__type']['fields']); + } + /** * Tests that Introspection Query with deprecated annotations on enum values, fields are read. */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php index fa7d1194c7f83..8b8973ad0fd95 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductWithCustomOptionsToCartTest.php @@ -58,7 +58,11 @@ public function testAddDownloadableProductWithOptions() $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); /* Generate customizable options fragment for GraphQl request */ - $queryCustomizableOptionValues = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + $queryCustomizableOptionValues = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode(array_values($customOptionsValues)) + ); $customizableOptions = "customizable_options: {$queryCustomizableOptionValues}"; $query = $this->getQuery($maskedQuoteId, $qty, $sku, $customizableOptions, $linkId); @@ -68,13 +72,14 @@ public function testAddDownloadableProductWithOptions() self::assertCount($qty, $response['addDownloadableProductsToCart']['cart']); $customizableOptionsOutput = $response['addDownloadableProductsToCart']['cart']['items'][0]['customizable_options']; - $assignedOptionsCount = count($customOptionsValues); - for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { - $expectedValues = $this->buildExpectedValuesArray($customOptionsValues[$counter]['value_string']); + $count = 0; + foreach ($customOptionsValues as $value) { + $expectedValues = $this->buildExpectedValuesArray($value['value_string']); self::assertEquals( $expectedValues, - $customizableOptionsOutput[$counter]['values'] + $customizableOptionsOutput[$count]['values'] ); + $count++; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php index b0b116b0cddad..5c2bc10bf771e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php @@ -57,7 +57,11 @@ public function testAddSimpleProductWithOptions() $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); /* Generate customizable options fragment for GraphQl request */ - $queryCustomizableOptionValues = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + $queryCustomizableOptionValues = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode(array_values($customOptionsValues)) + ); $customizableOptions = "customizable_options: {$queryCustomizableOptionValues}"; $query = $this->getQuery($maskedQuoteId, $sku, $quantity, $customizableOptions); @@ -68,13 +72,14 @@ public function testAddSimpleProductWithOptions() self::assertCount(1, $response['addSimpleProductsToCart']['cart']); $customizableOptionsOutput = $response['addSimpleProductsToCart']['cart']['items'][0]['customizable_options']; - $assignedOptionsCount = count($customOptionsValues); - for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { - $expectedValues = $this->buildExpectedValuesArray($customOptionsValues[$counter]['value_string']); + $count = 0; + foreach ($customOptionsValues as $type => $value) { + $expectedValues = $this->buildExpectedValuesArray($value['value_string'], $type); self::assertEquals( $expectedValues, - $customizableOptionsOutput[$counter]['values'] + $customizableOptionsOutput[$count]['values'] ); + $count++; } } @@ -99,6 +104,33 @@ public function testAddSimpleProductWithMissedRequiredOptionsSet() $this->graphQlMutation($query); } + /** + * Test adding a simple product with wrong format value for date option + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithWrongDateOptionFormat() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'simple'; + $quantity = 1; + + $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); + $customOptionsValues['date']['value_string'] = '12-12-12'; + $queryCustomizableOptionValues = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode(array_values($customOptionsValues)) + ); + $customizableOptions = "customizable_options: {$queryCustomizableOptionValues}"; + $query = $this->getQuery($maskedQuoteId, $sku, $quantity, $customizableOptions); + + $this->expectExceptionMessage('Invalid format provided. Please use \'Y-m-d H:i:s\' format.'); + + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $sku @@ -145,10 +177,14 @@ private function getQuery(string $maskedQuoteId, string $sku, float $quantity, s * Build the part of expected response. * * @param string $assignedValue + * @param string $type option type * @return array */ - private function buildExpectedValuesArray(string $assignedValue) : array + private function buildExpectedValuesArray(string $assignedValue, string $type) : array { + if ($type === 'date') { + return [['value' => date('M d, Y', strtotime($assignedValue))]]; + } $assignedOptionsArray = explode(',', trim($assignedValue, '[]')); $expectedArray = []; foreach ($assignedOptionsArray as $assignedOption) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php index a8088b0b46b87..561318889e325 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php @@ -57,7 +57,11 @@ public function testAddVirtualProductWithOptions() $customOptionsValues = $this->getCustomOptionsValuesForQueryBySku->execute($sku); /* Generate customizable options fragment for GraphQl request */ - $queryCustomizableOptionValues = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + $queryCustomizableOptionValues = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode(array_values($customOptionsValues)) + ); $customizableOptions = "customizable_options: {$queryCustomizableOptionValues}"; $query = $this->getQuery($maskedQuoteId, $sku, $quantity, $customizableOptions); @@ -68,13 +72,14 @@ public function testAddVirtualProductWithOptions() self::assertCount(1, $response['addVirtualProductsToCart']['cart']); $customizableOptionsOutput = $response['addVirtualProductsToCart']['cart']['items'][0]['customizable_options']; - $assignedOptionsCount = count($customOptionsValues); - for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { - $expectedValues = $this->buildExpectedValuesArray($customOptionsValues[$counter]['value_string']); + $count = 0; + foreach ($customOptionsValues as $value) { + $expectedValues = $this->buildExpectedValuesArray($value['value_string']); self::assertEquals( $expectedValues, - $customizableOptionsOutput[$counter]['values'] + $customizableOptionsOutput[$count]['values'] ); + $count++; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php new file mode 100644 index 0000000000000..2588de97bad7d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php @@ -0,0 +1,549 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; + +/** + * Test cases for applying cart promotions to items in cart + */ +class CartPromotionsTest extends GraphQlAbstract +{ + /** + * Test adding single cart rule to multiple products in a cart + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php + */ + public function testCartPromotionSingleCartRule() + { + $skus =['simple1', 'simple2']; + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $categoryId = 66; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [$categoryId] + ); + } + /** @var Collection $ruleCollection */ + $ruleCollection = $objectManager->get(Collection::class); + $ruleLabels = []; + /** @var Rule $rule */ + foreach ($ruleCollection as $rule) { + $ruleLabels = $rule->getStoreLabels(); + } + $qty = 2; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + $productsInCart = [$prod1, $prod2]; + //validating the line item prices, quantity and discount + $this->assertLineItemDiscountPrices($response, $productsInCart, $qty, $ruleLabels); + //total discount on the cart which is the sum of the individual row discounts + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 21.98); + } + + /** + * Assert the row total discounts and individual discount break down and cart rule labels + * + * @param $response + * @param $productsInCart + * @param $qty + * @param $ruleLabels + */ + private function assertLineItemDiscountPrices($response, $productsInCart, $qty, $ruleLabels) + { + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'row_total_including_tax' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'total_item_discount' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5], + 'discounts' => [ + 0 =>[ + 'amount' => + ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5], + 'label' => $ruleLabels[0] + ] + ] + ], + ] + ); + } + } + + /** + * Apply multiple cart rules to multiple products in a cart + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items.php + */ + public function testCartPromotionsMultipleCartRules() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $productsInCart = [$prod1, $prod2]; + $skus =['simple1', 'simple2']; + $categoryId = 66; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [$categoryId] + ); + } + /** @var Collection $ruleCollection */ + $ruleCollection = $objectManager->get(Collection::class); + $ruleLabels = []; + /** @var Rule $rule */ + foreach ($ruleCollection as $rule) { + $ruleLabels[] = $rule->getStoreLabels(); + } + $qty = 2; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + + //validating the individual discounts per line item and total discounts per line item + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $lineItemDiscount = $productsInResponse[$itemIndex][0]['prices']['discounts']; + $expectedTotalDiscountValue = ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5) + + ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5*0.1); + $this->assertEquals( + $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5, + current($lineItemDiscount)['amount']['value'] + ); + $this->assertEquals('TestRule_Label', current($lineItemDiscount)['label']); + + $lineItemDiscountValue = next($lineItemDiscount)['amount']['value']; + $this->assertEquals( + round($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5)*0.1, + $lineItemDiscountValue + ); + $this->assertEquals('10% off with two items_Label', end($lineItemDiscount)['label']); + $actualTotalDiscountValue = $lineItemDiscount[0]['amount']['value']+$lineItemDiscount[1]['amount']['value']; + $this->assertEquals(round($expectedTotalDiscountValue, 2), $actualTotalDiscountValue); + + //removing the elements from the response so that the rest of the response values can be compared + unset($productsInResponse[$itemIndex][0]['prices']['discounts']); + unset($productsInResponse[$itemIndex][0]['prices']['total_item_discount']); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'row_total_including_tax' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty] + ], + ] + ); + } + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 21.98); + $this->assertEquals($response['cart']['prices']['discounts'][1]['amount']['value'], 2.2); + } + + /** + * Apply cart rules to multiple products in a cart with taxes + * Tax settings : Including and Excluding tax for Price Display and Shopping cart display + * Discount on Prices Includes Tax + * Tax rate = 7.5% + * Cart rule to apply 50% for products assigned to a specific category + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings.php + * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php + */ + public function testCartPromotionsCartRulesWithTaxes() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $productsInCart = [$prod1, $prod2]; + $skus =['simple1', 'simple2']; + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + foreach ($productsInCart as $product) { + $product->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product); + } + $categoryId = 66; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [$categoryId] + ); + } + $qty = 1; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $this->setShippingAddressOnCart($cartId); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $rowTotalIncludingTax = round( + $productsInCart[$itemIndex]->getSpecialPrice()*$qty + + $productsInCart[$itemIndex]->getSpecialPrice()*$qty*.075, + 2 + ); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + // row_total is the line item price without the tax + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + // row_total including tax is the price + price * tax rate + 'row_total_including_tax' => ['value' => $rowTotalIncludingTax], + // discount from cart rule after tax is applied : 50% of row_total_including_tax + 'total_item_discount' => ['value' => round($rowTotalIncludingTax/2, 2)], + 'discounts' => [ + 0 =>[ + 'amount' => + ['value' => round($rowTotalIncludingTax/2, 2)], + 'label' => 'TestRule_Label' + ] + ] + ], + ] + ); + } + // checking the total discount on the entire cart + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 11.82); + } + + /** + * Apply cart rule with a fixed discount when specific coupon code + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testCartPromotionsWithCoupons() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $productsInCart = [$prod1, $prod2]; + + $skus =['simple1', 'simple2']; + + /** @var Collection $ruleCollection */ + $ruleCollection = $objectManager->get(Collection::class); + $ruleLabels = []; + /** @var Rule $rule */ + foreach ($ruleCollection as $rule) { + $ruleLabels = $rule->getStoreLabels(); + } + $qty = 2; + // coupon code obtained from the fixture + $couponCode = '2?ds5!2d'; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $this->applyCouponsToCart($cartId, $couponCode); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $sumOfPricesForBothProducts = 43.96; + $rowTotal = ($productsInCart[$itemIndex]->getSpecialPrice()*$qty); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'row_total_including_tax' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'total_item_discount' => ['value' => round(($rowTotal/$sumOfPricesForBothProducts)*5, 2)], + 'discounts' => [ + 0 =>[ + 'amount' => + ['value' => round(($rowTotal/$sumOfPricesForBothProducts)*5, 2)], + 'label' => $ruleLabels[0] + ] + ] + ], + ] + ); + } + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 5); + } + + /** + * If no discount is applicable to the cart, row total discount should be zero and no rule label shown + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/buy_3_get_1_free.php + */ + public function testCartPromotionsWhenNoDiscountIsAvailable() + { + $skus =['simple1', 'simple2']; + $qty = 2; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + foreach ($response['cart']['items'] as $cartItems) { + $this->assertEquals(0, $cartItems['prices']['total_item_discount']['value']); + $this->assertNull($cartItems['prices']['discounts']); + } + } + + /** + * Validating if the discount label in the response shows the default value if no label is available on cart rule + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off.php + */ + public function testCartPromotionsWithNoRuleLabels() + { + $skus =['simple1', 'simple2']; + $qty = 1; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + //total items added to cart + $this->assertCount(2, $response['cart']['items']); + //checking the default label for individual line item when cart rule doesn't have a label set + foreach ($response['cart']['items'] as $cartItem) { + $this->assertEquals('Discount', $cartItem['prices']['discounts'][0]['label']); + } + } + + /** + * Apply coupon to the cart + * + * @param string $cartId + * @param string $couponCode + */ + private function applyCouponsToCart(string $cartId, string $couponCode) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$cartId", coupon_code: "$couponCode"}) { + cart { + applied_coupons { + code + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + } + + /** + * @param string $cartId + * @return string + */ + private function getCartItemPricesQuery(string $cartId): string + { + return <<<QUERY +{ + cart(cart_id:"{$cartId}"){ + items{ + quantity + prices{ + row_total{ + value + } + row_total_including_tax{ + value + } + total_item_discount{value} + discounts{ + amount{value} + label + } + } + } + prices{ + discounts{ + amount{value} + } + + } + } +} + +QUERY; + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlMutation($query); + $cartId = $response['createEmptyCart']; + return $cartId; + } + + /** + * @param string $cartId + * @param int $sku1 + * @param int $qty + * @param string $sku2 + */ + private function addMultipleSimpleProductsToCart(string $cartId, int $qty, string $sku1, string $sku2): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart(input: { + cart_id: "{$cartId}", + cart_items: [ + { + data: { + quantity: $qty + sku: "$sku1" + } + } + { + data: { + quantity: $qty + sku: "$sku2" + } + } + ] + } + ) { + cart { + items { + product{sku} + quantity + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku1, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][1]['quantity']); + self::assertEquals($sku2, $response['addSimpleProductsToCart']['cart']['items'][1]['product']['sku']); + } + + /** + * Set shipping address for the region for which tax rule is set + * + * @param string $cartId + * @return void + */ + private function setShippingAddressOnCart(string $cartId) :void + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "John" + lastname: "Doe" + company: "Magento" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36043" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + region{label} + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertEquals( + 'Montgomery', + $response['setShippingAddressesOnCart']['cart']['shipping_addresses'][0]['city'] + ); + self::assertEquals( + 'Alabama', + $response['setShippingAddressesOnCart']['cart']['shipping_addresses'][0]['region']['label'] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php index e1b93e0bdb857..0c676d86a33da 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php @@ -29,7 +29,7 @@ class AddSimpleProductToCartTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; - protected function setUp() + protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); @@ -50,14 +50,42 @@ public function testAddSimpleProductToCart() $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + self::assertArrayHasKey('shipping_addresses', $response['addSimpleProductsToCart']['cart']); + self::assertEmpty($response['addSimpleProductsToCart']['cart']['shipping_addresses']); self::assertEquals($quantity, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + self::assertArrayHasKey('prices', $response['addSimpleProductsToCart']['cart']['items'][0]); + + self::assertArrayHasKey('price', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $price = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['price']; + self::assertArrayHasKey('value', $price); + self::assertEquals(10, $price['value']); + self::assertArrayHasKey('currency', $price); + self::assertEquals('USD', $price['currency']); + + self::assertArrayHasKey('row_total', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $rowTotal = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total']; + self::assertArrayHasKey('value', $rowTotal); + self::assertEquals(20, $rowTotal['value']); + self::assertArrayHasKey('currency', $rowTotal); + self::assertEquals('USD', $rowTotal['currency']); + + self::assertArrayHasKey( + 'row_total_including_tax', + $response['addSimpleProductsToCart']['cart']['items'][0]['prices'] + ); + $rowTotalIncludingTax = + $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total_including_tax']; + self::assertArrayHasKey('value', $rowTotalIncludingTax); + self::assertEquals(20, $rowTotalIncludingTax['value']); + self::assertArrayHasKey('currency', $rowTotalIncludingTax); + self::assertEquals('USD', $rowTotalIncludingTax['currency']); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field AddSimpleProductsToCartInput.cart_id of required type String! was not provided. */ public function testAddSimpleProductToCartIfCartIdIsMissed() { @@ -110,7 +138,6 @@ public function testAddSimpleProductToCartIfCartIdIsEmpty() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_items" is missing */ public function testAddSimpleProductToCartIfCartItemsAreMissed() { @@ -130,6 +157,11 @@ public function testAddSimpleProductToCartIfCartItemsAreMissed() } QUERY; + $this->expectExceptionMessage( + 'Field AddSimpleProductsToCartInput.cart_items of required type' + . ' [SimpleProductCartItemInput]! was not provided.' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } @@ -262,6 +294,34 @@ private function getQuery(string $maskedQuoteId, string $sku, float $quantity): product { sku } + prices { + price { + value + currency + } + row_total { + value + currency + } + row_total_including_tax { + value + currency + } + } + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php index ae0208ca3101b..a7a3028f2a369 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php @@ -57,7 +57,7 @@ public function testAddVirtualProductToCart() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field AddSimpleProductsToCartInput.cart_id of required type String! was not provided. */ public function testAddVirtualProductToCartIfCartIdIsMissed() { @@ -109,8 +109,6 @@ public function testAddVirtualProductToCartIfCartIdIsEmpty() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_items" is missing */ public function testAddVirtualProductToCartIfCartItemsAreMissed() { @@ -129,6 +127,11 @@ public function testAddVirtualProductToCartIfCartItemsAreMissed() } } QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'Field AddSimpleProductsToCartInput.cart_items of required type [SimpleProductCartItemInput]!' + . ' was not provided.' + ); $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php index df138f9014706..fa96443eaee1e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php @@ -217,11 +217,11 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_id' => [ 'coupon_code: "test"', - 'Required parameter "cart_id" is missing' + 'Field ApplyCouponToCartInput.cart_id of required type String! was not provided.' ], 'missed_coupon_code' => [ 'cart_id: "test_quote"', - 'Required parameter "coupon_code" is missing' + 'Field ApplyCouponToCartInput.coupon_code of required type String! was not provided.' ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php index 5a4cc88d69623..ddf94fbcc1edf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php @@ -158,7 +158,7 @@ private function findProduct(): string products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 1 @@ -259,7 +259,6 @@ private function setBillingAddress(string $cartId): void telephone: "88776655" region: "TX" country_code: "US" - save_in_address_book: false } } } @@ -298,7 +297,6 @@ private function setShippingAddress(string $cartId): array postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -426,7 +424,7 @@ private function placeOrder(string $cartId): string } ) { order { - order_id + order_number } } } @@ -434,10 +432,10 @@ private function placeOrder(string $cartId): string $response = $this->graphQlMutation($query, [], '', $this->headers); self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertNotEmpty($response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_number']); - return $response['placeOrder']['order']['order_id']; + return $response['placeOrder']['order']['order_number']; } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartIsVirtualTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartIsVirtualTest.php new file mode 100644 index 0000000000000..cf72435a123bf --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartIsVirtualTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting is_virtual from cart + */ +class GetCartIsVirtualTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetMixedCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCartIsVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertTrue($response['cart']['is_virtual']); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id:"$maskedQuoteId") { + is_virtual + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php index e7f87f362044a..7ffce2a7f541d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -65,6 +65,8 @@ public function testGetCart() $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('id', $response['cart']); + self::assertEquals($maskedQuoteId, $response['cart']['id']); self::assertArrayHasKey('items', $response['cart']); self::assertCount(2, $response['cart']['items']); @@ -192,7 +194,8 @@ public function testGetCartWithNotDefaultStore() * @magentoApiDataFixture Magento/Store/_files/second_store.php * * @expectedException Exception - * @expectedExceptionMessage Wrong store code specified for cart + * @expectedExceptionMessage The account sign-in was incorrect or your account is disabled temporarily. + * Please wait and try again later. */ public function testGetCartWithWrongStore() { @@ -255,6 +258,7 @@ private function getQuery(string $maskedQuoteId): string return <<<QUERY { cart(cart_id: "{$maskedQuoteId}") { + id items { id quantity diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCustomerCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCustomerCartTest.php new file mode 100644 index 0000000000000..8100bce4ac718 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCustomerCartTest.php @@ -0,0 +1,244 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting Customer cart information + */ +class GetCustomerCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * Query for an existing active customer cart + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetActiveCustomerCart() + { + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $customerCartQuery = $this->getCustomerCartQuery(); + $response = $this->graphQlQuery($customerCartQuery, [], '', $this->getHeaderMap()); + $this->assertArrayHasKey('customerCart', $response); + $this->assertArrayHasKey('items', $response['customerCart']); + $this->assertNotEmpty($response['customerCart']['items']); + $this->assertEquals(2, $response['customerCart']['total_quantity']); + $this->assertArrayHasKey('id', $response['customerCart']); + $this->assertNotEmpty($response['customerCart']['id']); + $this->assertEquals($maskedQuoteId, $response['customerCart']['id']); + $this->assertEquals( + $quantity, + $response['customerCart']['items'][0]['quantity'], + 'Incorrect quantity of products in cart' + ); + } + + /** + * Query for an existing customer cart with no masked quote id + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart_without_masked_quote_id.php + */ + public function testGetLoggedInCustomerCartWithoutMaskedQuoteId() + { + $customerCartQuery = $this->getCustomerCartQuery(); + $response = $this->graphQlQuery($customerCartQuery, [], '', $this->getHeaderMap()); + $this->assertArrayHasKey('customerCart', $response); + $this->assertArrayHasKey('items', $response['customerCart']); + $this->assertEmpty($response['customerCart']['items']); + $this->assertEquals(0, $response['customerCart']['total_quantity']); + $this->assertArrayHasKey('id', $response['customerCart']); + $this->assertNotEmpty($response['customerCart']['id']); + $this->assertNotNull($response['customerCart']['id']); + } + + /** + * Query for customer cart for a user with no existing active cart + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testGetNewCustomerCart() + { + $customerCartQuery = $this->getCustomerCartQuery(); + $response = $this->graphQlQuery($customerCartQuery, [], '', $this->getHeaderMap()); + $this->assertArrayHasKey('customerCart', $response); + $this->assertArrayHasKey('id', $response['customerCart']); + $this->assertNotNull($response['customerCart']['id']); + $this->assertNotEmpty($response['customerCart']['id']); + $this->assertEmpty($response['customerCart']['items']); + $this->assertEquals(0, $response['customerCart']['total_quantity']); + } + + /** + * Query for customer cart with no customer token passed + * + * @expectedException Exception + * @expectedExceptionMessage The request is allowed for logged in customer + */ + public function testGetCustomerCartWithNoCustomerToken() + { + $customerCartQuery = $this->getCustomerCartQuery(); + $this->graphQlQuery($customerCartQuery); + } + + /** + * Query for customer cart after customer token is revoked + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage The request is allowed for logged in customer + */ + public function testGetCustomerCartAfterTokenRevoked() + { + $customerCartQuery = $this->getCustomerCartQuery(); + $headers = $this->getHeaderMap(); + $response = $this->graphQlMutation($customerCartQuery, [], '', $headers); + $this->assertArrayHasKey('customerCart', $response); + $this->assertArrayHasKey('id', $response['customerCart']); + $this->assertNotNull($response['customerCart']['id']); + $this->assertNotEmpty($response['customerCart']['id']); + $this->revokeCustomerToken(); + $customerCartQuery = $this->getCustomerCartQuery(); + $this->graphQlQuery($customerCartQuery, [], '', $headers); + } + + /** + * Querying for the customer cart twice->should return the same cart + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testRequestCustomerCartTwice() + { + $customerCartQuery = $this->getCustomerCartQuery(); + $response = $this->graphQlMutation($customerCartQuery, [], '', $this->getHeaderMap()); + $this->assertArrayHasKey('customerCart', $response); + $this->assertArrayHasKey('id', $response['customerCart']); + $this->assertNotNull($response['customerCart']['id']); + $cartId = $response['customerCart']['id']; + $customerCartQuery = $this->getCustomerCartQuery(); + $response2 = $this->graphQlQuery($customerCartQuery, [], '', $this->getHeaderMap()); + $this->assertEquals($cartId, $response2['customerCart']['id']); + } + + /** + * Query for inactive Customer cart - in case of not finding an active cart, it should create a new one + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + */ + public function testGetInactiveCustomerCart() + { + $customerCartQuery = $this->getCustomerCartQuery(); + $response = $this->graphQlQuery($customerCartQuery, [], '', $this->getHeaderMap()); + $this->assertArrayHasKey('customerCart', $response); + $this->assertNotEmpty($response['customerCart']['id']); + $this->assertEmpty($response['customerCart']['items']); + $this->assertEmpty($response['customerCart']['total_quantity']); + } + + /** + * Querying for an existing customer cart for second store + * + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_customer_not_default_store.php + */ + public function testGetCustomerCartSecondStore() + { + $maskedQuoteIdSecondStore = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store'); + $customerCartQuery = $this->getCustomerCartQuery(); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'fixture_second_store'; + $responseSecondStore = $this->graphQlQuery($customerCartQuery, [], '', $headerMap); + $this->assertEquals($maskedQuoteIdSecondStore, $responseSecondStore['customerCart']['id']); + } + + /** + * Query to revoke customer token + * + * @return void + */ + private function revokeCustomerToken(): void + { + $query = <<<QUERY +mutation{ + revokeCustomerToken{ + result + } +} +QUERY; + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Query customer cart + * + * @return string + */ + private function getCustomerCartQuery(): string + { + return <<<QUERY +{ + customerCart { + total_quantity + id + items { + id + quantity + product { + sku + } + } + } +} +QUERY; + } + + /** + * Create a header with customer token + * + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php index 1fafc753e498e..f5700d27fea7a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php @@ -100,18 +100,7 @@ public function testGetSelectedShippingMethodBeforeSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - - self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - - self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - - self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** @@ -174,12 +163,7 @@ public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); - self::assertNull($shippingAddress['selected_shipping_method']['amount']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php index b6dde46871cbb..e5353fc841c5d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php @@ -91,29 +91,7 @@ public function testGetSpecifiedBillingAddressIfBillingAddressIsNotSet() $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('billing_address', $response['cart']); - - $expectedBillingAddressData = [ - 'firstname' => null, - 'lastname' => null, - 'company' => null, - 'street' => [ - '' - ], - 'city' => null, - 'region' => [ - 'code' => null, - 'label' => null, - ], - 'postcode' => null, - 'country' => [ - 'code' => null, - 'label' => null, - ], - 'telephone' => null, - '__typename' => 'BillingCartAddress', - 'customer_notes' => null, - ]; - self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + self::assertNull($response['cart']['billing_address']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php index de3c384e65783..2023603a21eed 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php @@ -92,28 +92,7 @@ public function testGetSpecifiedShippingAddressIfShippingAddressIsNotSet() self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('shipping_addresses', $response['cart']); - $expectedShippingAddressData = [ - 'firstname' => null, - 'lastname' => null, - 'company' => null, - 'street' => [ - '' - ], - 'city' => null, - 'region' => [ - 'code' => null, - 'label' => null, - ], - 'postcode' => null, - 'country' => [ - 'code' => null, - 'label' => null, - ], - 'telephone' => null, - '__typename' => 'ShippingCartAddress', - 'customer_notes' => null, - ]; - self::assertEquals($expectedShippingAddressData, current($response['cart']['shipping_addresses'])); + self::assertEquals([], $response['cart']['shipping_addresses']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php new file mode 100644 index 0000000000000..b78c8894970b5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php @@ -0,0 +1,293 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Test for merging customer carts + */ +class MergeCartsTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + protected function tearDown() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, '1', 'customer_id'); + $this->quoteResource->delete($quote); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testMergeGuestWithCustomerCart() + { + $customerQuote = $this->quoteFactory->create(); + $this->quoteResource->load($customerQuote, 'test_quote', 'reserved_order_id'); + + $guestQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $guestQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId()); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId()); + + $query = $this->getCartMergeMutation($guestQuoteMaskedId, $customerQuoteMaskedId); + $mergeResponse = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('mergeCarts', $mergeResponse); + $cartResponse = $mergeResponse['mergeCarts']; + self::assertArrayHasKey('items', $cartResponse); + self::assertCount(2, $cartResponse['items']); + $cartResponse = $this->graphQlMutation( + $this->getCartQuery($customerQuoteMaskedId), + [], + '', + $this->getHeaderMap() + ); + + self::assertArrayHasKey('cart', $cartResponse); + self::assertArrayHasKey('items', $cartResponse['cart']); + self::assertCount(2, $cartResponse['cart']['items']); + $item1 = $cartResponse['cart']['items'][0]; + self::assertArrayHasKey('quantity', $item1); + self::assertEquals(2, $item1['quantity']); + $item2 = $cartResponse['cart']['items'][1]; + self::assertArrayHasKey('quantity', $item2); + self::assertEquals(1, $item2['quantity']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage Current user does not have an active cart. + */ + public function testGuestCartExpiryAfterMerge() + { + $customerQuote = $this->quoteFactory->create(); + $this->quoteResource->load($customerQuote, 'test_quote', 'reserved_order_id'); + + $guestQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $guestQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId()); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId()); + + $query = $this->getCartMergeMutation($guestQuoteMaskedId, $customerQuoteMaskedId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $this->graphQlMutation( + $this->getCartQuery($guestQuoteMaskedId), + [], + '', + $this->getHeaderMap() + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The current user cannot perform operations on cart + */ + public function testMergeTwoCustomerCarts() + { + $firstQuote = $this->quoteFactory->create(); + $this->quoteResource->load($firstQuote, 'test_quote', 'reserved_order_id'); + $firstMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId()); + + $createCartResponse = $this->graphQlMutation( + $this->getCreateEmptyCartMutation(), + [], + '', + $this->getHeaderMap('customer_two@example.com') + ); + self::assertArrayHasKey('createEmptyCart', $createCartResponse); + $secondMaskedId = $createCartResponse['createEmptyCart']; + $this->addSimpleProductToCart($secondMaskedId, $this->getHeaderMap()); + + $query = $this->getCartMergeMutation($firstMaskedId, $secondMaskedId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Add simple product to cart + * + * @param string $maskedId + * @param array $headerMap + * @throws \Exception + */ + private function addSimpleProductToCart(string $maskedId, array $headerMap): void + { + $result = $this->graphQlMutation($this->getAddProductToCartMutation($maskedId), [], '', $headerMap); + self::assertArrayHasKey('addSimpleProductsToCart', $result); + self::assertArrayHasKey('cart', $result['addSimpleProductsToCart']); + self::assertArrayHasKey('items', $result['addSimpleProductsToCart']['cart']); + self::assertArrayHasKey(0, $result['addSimpleProductsToCart']['cart']['items']); + self::assertArrayHasKey('quantity', $result['addSimpleProductsToCart']['cart']['items'][0]); + self::assertEquals(1, $result['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertArrayHasKey('product', $result['addSimpleProductsToCart']['cart']['items'][0]); + self::assertArrayHasKey('sku', $result['addSimpleProductsToCart']['cart']['items'][0]['product']); + self::assertEquals('simple_product', $result['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * Create the mergeCart mutation + * + * @param string $guestQuoteMaskedId + * @param string $customerQuoteMaskedId + * @return string + */ + private function getCartMergeMutation(string $guestQuoteMaskedId, string $customerQuoteMaskedId): string + { + return <<<QUERY +mutation { + mergeCarts( + source_cart_id: "{$guestQuoteMaskedId}" + destination_cart_id: "{$customerQuoteMaskedId}" + ){ + items { + quantity + product { + sku + } + } + } +} +QUERY; + } + + /** + * Get cart query + * + * @param string $maskedId + * @return string + */ + private function getCartQuery(string $maskedId): string + { + return <<<QUERY +{ + cart(cart_id: "{$maskedId}") { + items { + quantity + product { + sku + } + } + } +} +QUERY; + } + + /** + * Get create empty cart mutation + * + * @return string + */ + private function getCreateEmptyCartMutation(): string + { + return <<<QUERY +mutation { + createEmptyCart +} +QUERY; + } + + /** + * Get add product to cart mutation + * + * @param string $maskedId + * @return string + */ + private function getAddProductToCartMutation(string $maskedId): string + { + return <<<QUERY +mutation { + addSimpleProductsToCart(input: { + cart_id: "{$maskedId}" + cart_items: { + data: { + quantity: 1 + sku: "simple_product" + } + } + }) { + cart { + items { + quantity + product { + sku + } + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php index cb471d8f0f936..189d5ceab838d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php @@ -86,8 +86,8 @@ public function testPlaceOrder() $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('placeOrder', $response); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } /** @@ -106,7 +106,7 @@ public function testPlaceOrderIfCartIdIsEmpty() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field PlaceOrderInput.cart_id of required type String! was not provided. */ public function testPlaceOrderIfCartIdIsMissed() { @@ -114,7 +114,7 @@ public function testPlaceOrderIfCartIdIsMissed() mutation { placeOrder(input: {}) { order { - order_id + order_number } } } @@ -313,7 +313,7 @@ private function getQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php index f906b33fe19d1..d4390e902a3f9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php @@ -73,7 +73,7 @@ public function testRemoveCouponFromCartIfCartIdIsEmpty() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field RemoveCouponFromCartInput.cart_id of required type String! was not provided. */ public function testRemoveCouponFromCartIfCartIdIsMissed() { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php index b810022403b65..a14aacc974af6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php @@ -125,11 +125,11 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_id' => [ 'cart_item_id: 1', - 'Required parameter "cart_id" is missing.' + 'Field RemoveItemFromCartInput.cart_id of required type String! was not provided.' ], 'missed_cart_item_id' => [ 'cart_id: "test_quote"', - 'Required parameter "cart_item_id" is missing.' + 'Field RemoveItemFromCartInput.cart_item_id of required type Int! was not provided.' ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php index 011930e723273..19a10d9466a32 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -7,6 +7,9 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; @@ -45,6 +48,19 @@ class SetBillingAddressOnCartTest extends GraphQlAbstract */ private $customerTokenService; + /** + * @var AddressRepositoryInterface + */ + private $customerAddressRepository; + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + protected function setUp() { $objectManager = Bootstrap::getObjectManager(); @@ -53,6 +69,9 @@ protected function setUp() $this->quoteFactory = $objectManager->get(QuoteFactory::class); $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->customerAddressRepository = $objectManager->get(AddressRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); } /** @@ -77,12 +96,12 @@ public function testSetNewBillingAddress() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } + same_as_shipping: true } } ) { @@ -101,6 +120,20 @@ public function testSetNewBillingAddress() } __typename } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } } } } @@ -111,10 +144,15 @@ public function testSetNewBillingAddress() $cartResponse = $response['setBillingAddressOnCart']['cart']; self::assertArrayHasKey('billing_address', $cartResponse); $billingAddressResponse = $cartResponse['billing_address']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'ShippingCartAddress'); } /** + * Test case for deprecated `use_for_shipping` param. + * * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php @@ -136,11 +174,10 @@ public function testSetNewBillingAddressWithUseForShippingParameter() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } use_for_shipping: true } @@ -239,6 +276,42 @@ public function testSetBillingAddressFromAddressBook() $this->assertSavedBillingAddressFields($billingAddressResponse); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testVerifyBillingAddressType() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $billingAddress = $response['setBillingAddressOnCart']['cart']['billing_address']; + self::assertArrayHasKey('__typename', $billingAddress); + self::assertEquals('BillingCartAddress', $billingAddress['__typename']); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php @@ -297,11 +370,10 @@ public function testSetNewBillingAddressAndFromAddressBookAtSameTime() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -337,7 +409,7 @@ public function testSetNewBillingAddressWithoutCustomerAddressIdAndAddress() input: { cart_id: "$maskedQuoteId" billing_address: { - use_for_shipping: true + same_as_shipping: true } } ) { @@ -363,7 +435,7 @@ public function testSetNewBillingAddressWithoutCustomerAddressIdAndAddress() * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_multishipping_with_two_shipping_addresses.php */ - public function testSetNewBillingAddressWithUseForShippingAndMultishipping() + public function testSetNewBillingAddressWithSameAsShippingAndMultishipping() { $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -379,13 +451,12 @@ public function testSetNewBillingAddressWithUseForShippingAndMultishipping() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } - use_for_shipping: true + same_as_shipping: true } } ) { @@ -399,7 +470,7 @@ public function testSetNewBillingAddressWithUseForShippingAndMultishipping() QUERY; self::expectExceptionMessage( - 'Using the "use_for_shipping" option with multishipping is not possible.' + 'Using the "same_as_shipping" option with multishipping is not possible.' ); $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } @@ -573,7 +644,7 @@ public function testSetBillingAddressWithoutRequiredParameters(string $input, st QUERY; $this->expectExceptionMessage($message); - $this->graphQlMutation($query); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } /** @@ -589,8 +660,56 @@ public function dataProviderSetWithoutRequiredParameters(): array ], 'missed_cart_id' => [ 'billing_address: {}', - 'Required parameter "cart_id" is missing' - ] + 'Field SetBillingAddressOnCartInput.cart_id of required type String! was not provided.' + ], + 'missed_region' => [ + 'cart_id: "cart_id_value" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + postcode: "887766" + country_code: "US" + telephone: "88776655" + } + }', + '"regionId" is required. Enter and try again.' + ], + 'missed_multiple_fields' => [ + 'cart_id: "cart_id_value" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + country_code: "US" + telephone: "88776655" + } + }', + '"postcode" is required. Enter and try again. +"regionId" is required. Enter and try again.' + ], + 'wrong_required_region' => [ + 'cart_id: "cart_id_value" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + region: "wrong region" + city: "test city" + country_code: "US" + telephone: "88776655" + } + }', + 'Region is not available for the selected country' + ], ]; } @@ -616,11 +735,10 @@ public function testSetNewBillingAddressWithRedundantStreetLine() company: "test company" street: ["test street 1", "test street 2", "test street 3"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -659,11 +777,10 @@ public function testSetBillingAddressWithLowerCaseCountry() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "us" telephone: "88776655" - save_in_address_book: false } } } @@ -696,6 +813,242 @@ public function testSetBillingAddressWithLowerCaseCountry() $this->assertNewAddressFields($billingAddressResponse); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AZ" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: true + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(1, $addresses); + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithNotSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AZ" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(0, $addresses); + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testWithInvalidBillingAddressInput() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AZ" + postcode: "887766" + country_code: "USS" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + } + } + } +} +QUERY; + $this->expectExceptionMessage('Country is not available'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressesWithNotRequiredRegion() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "Vasyl" + lastname: "Doe" + street: ["1 Svobody"] + city: "Lviv" + region: "Lviv" + postcode: "00000" + country_code: "UA" + telephone: "555-555-55-55" + } + } + } + ) { + cart { + billing_address { + region { + label + } + country { + code + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertEquals('UA', $cartResponse['billing_address']['country']['code']); + self::assertEquals('Lviv', $cartResponse['billing_address']['region']['label']); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 12fb356904224..543ce6fe9c8e7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -78,9 +78,79 @@ public function testSetPaymentOnCartWithSimpleProduct() $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); - self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php + * + * @dataProvider dataProviderSetPaymentOnCartWithException + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetPaymentOnCartWithException(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setPaymentMethodAndPlaceOrder( + input: { + {$input} + } + ) { + order { + order_number + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderSetPaymentOnCartWithException(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: { + code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '" + }', + 'Field SetPaymentMethodAndPlaceOrderInput.cart_id of required type String! was not provided.', + ], + 'missed_payment_method' => [ + 'cart_id: "cart_id_value"', + 'Field SetPaymentMethodAndPlaceOrderInput.payment_method of required type PaymentMethodInput!' + . ' was not provided.', + ], + 'place_order_with_out_of_stock_products' => [ + 'cart_id: "cart_id_value" + payment_method: { + code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '" + }', + 'Unable to place order: Some of the products are out of stock.', + ], + ]; } /** @@ -116,9 +186,14 @@ public function testSetPaymentOnCartWithVirtualProduct() $query = $this->getQuery($maskedQuoteId, $methodCode); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); - self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); } /** @@ -224,14 +299,27 @@ private function getQuery( ) : string { return <<<QUERY mutation { - setPaymentMethodAndPlaceOrder(input: { - cart_id: "$maskedQuoteId" + setPaymentMethodOnCart( + input: { + cart_id: "{$maskedQuoteId}" payment_method: { - code: "$methodCode" + code: "{$methodCode}" } - }) { + } + ) { + cart { + selected_payment_method { + code + } + } + } + placeOrder( + input: { + cart_id: "{$maskedQuoteId}" + } + ) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php index efda719ca153c..da190be333600 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php @@ -235,11 +235,11 @@ public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array return [ 'missed_cart_id' => [ 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', - 'Required parameter "cart_id" is missing.' + 'Field SetPaymentMethodOnCartInput.cart_id of required type String! was not provided.' ], 'missed_payment_method' => [ 'cart_id: "cart_id_value"', - 'Required parameter "code" for "payment_method" is missing.' + 'Field SetPaymentMethodOnCartInput.payment_method of required type PaymentMethodInput! was not provided' ], 'missed_payment_method_code' => [ 'cart_id: "cart_id_value", payment_method: {code: ""}', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index 15ee125955062..47a3a13f05221 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -7,6 +7,9 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; @@ -45,6 +48,21 @@ class SetShippingAddressOnCartTest extends GraphQlAbstract */ private $customerTokenService; + /** + * @var AddressRepositoryInterface + */ + private $customerAddressRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + protected function setUp() { $objectManager = Bootstrap::getObjectManager(); @@ -53,6 +71,9 @@ protected function setUp() $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->customerAddressRepository = $objectManager->get(AddressRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); } /** @@ -78,12 +99,12 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } + customer_notes: "Test note" } ] } @@ -102,6 +123,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() code } __typename + customer_notes } } } @@ -142,11 +164,10 @@ public function testSetNewShippingAddressOnCartWithVirtualProduct() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -209,6 +230,43 @@ public function testSetShippingAddressFromAddressBook() $this->assertSavedShippingAddressFields($shippingAddressResponse); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testVerifyShippingAddressType() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $shippingAddresses = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('__typename', $shippingAddresses); + self::assertEquals('ShippingCartAddress', $shippingAddresses['__typename']); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php @@ -270,11 +328,10 @@ public function testSetNewShippingAddressAndFromAddressBookAtSameTime() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -404,6 +461,50 @@ public function testSetNewShippingAddressWithMissedRequiredParameters(string $in $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } + /** + * Covers case with empty street + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Field CartAddressInput.street of required type [String]! was not provided. + */ + public function testSetNewShippingAddressWithMissedRequiredStreetParameters() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + country_code: "US" + firstname: "J" + lastname: "D" + telephone: "+" + city: "C" + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + /** * @return array */ @@ -421,8 +522,56 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array ], 'missed_cart_id' => [ 'shipping_addresses: {}', - 'Required parameter "cart_id" is missing' - ] + 'Field SetShippingAddressesOnCartInput.cart_id of required type String! was not provided.' + ], + 'missed_region' => [ + 'cart_id: "cart_id_value" + shipping_addresses: [{ + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + postcode: "887766" + country_code: "US" + telephone: "88776655" + } + }]', + '"regionId" is required. Enter and try again.' + ], + 'missed_multiple_fields' => [ + 'cart_id: "cart_id_value" + shipping_addresses: [{ + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + country_code: "US" + telephone: "88776655" + } + }]', + '"postcode" is required. Enter and try again. +"regionId" is required. Enter and try again.' + ], + 'wrong_required_region' => [ + 'cart_id: "cart_id_value" + shipping_addresses: [{ + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + region: "wrong region" + city: "test city" + country_code: "US" + telephone: "88776655" + } + }]', + 'Region is not available for the selected country' + ], ]; } @@ -452,11 +601,10 @@ public function testSetMultipleNewShippingAddresses() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } }, { @@ -466,11 +614,10 @@ public function testSetMultipleNewShippingAddresses() company: "test company 2" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -510,11 +657,10 @@ public function testSetNewShippingAddressOnCartWithRedundantStreetLine() company: "test company" street: ["test street 1", "test street 2", "test street 3"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -655,6 +801,242 @@ public function testSetShippingAddressWithLowerCaseCountry() $this->assertEquals('CA', $address['region']['code']); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testWithInvalidShippingAddressesInput() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "John" + lastname: "Doe" + street: ["6161 West Centinella Avenue"] + city: "Culver City" + region: "CA" + postcode: "90230" + country_code: "USS" + telephone: "555-555-55-55" + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage('Country is not available'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressesWithNotRequiredRegion() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "Vasyl" + lastname: "Doe" + street: ["1 Svobody"] + city: "Lviv" + region: "Lviv" + postcode: "00000" + country_code: "UA" + telephone: "555-555-55-55" + } + } + ] + } + ) { + cart { + shipping_addresses { + region { + label + } + country { + code + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertEquals('UA', $cartResponse['shipping_addresses'][0]['country']['code']); + self::assertEquals('Lviv', $cartResponse['shipping_addresses'][0]['region']['label']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressWithSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AZ" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: true + } + customer_notes: "Test note" + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + customer_notes + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(1, $addresses); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressWithNotSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AZ" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + customer_notes: "Test note" + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + customer_notes + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(0, $addresses); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + /** * Verify the all the whitelisted fields for a New Address Object * @@ -671,7 +1053,8 @@ private function assertNewShippingAddressFields(array $shippingAddressResponse): ['response_field' => 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], - ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'] + ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'], + ['response_field' => 'customer_notes', 'expected_value' => 'Test note'] ]; $this->assertResponseFields($shippingAddressResponse, $assertionMap); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php index 278f21f30b72d..149a2fbb1da32 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -174,11 +174,12 @@ public function dataProviderSetShippingMethodWithWrongParameters(): array carrier_code: "flatrate" method_code: "flatrate" }]', - 'Required parameter "cart_id" is missing' + 'Field SetShippingMethodsOnCartInput.cart_id of required type String! was not provided.' ], 'missed_shipping_methods' => [ 'cart_id: "cart_id_value"', - 'Required parameter "shipping_methods" is missing' + 'Field SetShippingMethodsOnCartInput.shipping_methods of required type [ShippingMethodInput]!' + . ' was not provided.' ], 'shipping_methods_are_empty' => [ 'cart_id: "cart_id_value" shipping_methods: []', @@ -208,7 +209,7 @@ public function dataProviderSetShippingMethodWithWrongParameters(): array 'cart_id: "cart_id_value", shipping_methods: [{ carrier_code: "flatrate" }]', - 'Required parameter "method_code" is missing.' + 'Field ShippingMethodInput.method_code of required type String! was not provided.' ], 'empty_method_code' => [ 'cart_id: "cart_id_value", shipping_methods: [{ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php index 48ea4ab7a15e3..1ec2de36a0bc2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php @@ -212,7 +212,7 @@ public function testUpdateItemInAnotherCustomerCart() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException \Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing. + * @expectedExceptionMessage Field UpdateCartItemsInput.cart_id of required type String! was not provided. */ public function testUpdateWithMissedCartItemId() { @@ -277,11 +277,11 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_items' => [ '', - 'Required parameter "cart_items" is missing.' + 'Field UpdateCartItemsInput.cart_items of required type [CartItemUpdateInput]! was not provided.' ], 'missed_cart_item_id' => [ 'cart_items: [{ quantity: 2 }]', - 'Required parameter "cart_item_id" for "cart_items" is missing.' + 'Field CartItemUpdateInput.cart_item_id of required type Int! was not provided.' ], 'missed_cart_item_qty' => [ 'cart_items: [{ cart_item_id: 1 }]', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php index 62c1ae0dab3c7..d1edf742931c3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php @@ -9,7 +9,7 @@ use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\Exception\NoSuchEntityException as NoSuchEntityException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Model\Quote\Item; use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsValuesForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsValuesForQueryBySku.php index 7514eb1c4e1d0..62cacd3e07c16 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsValuesForQueryBySku.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsValuesForQueryBySku.php @@ -40,25 +40,26 @@ public function execute(string $sku): array foreach ($customOptions as $customOption) { $optionType = $customOption->getType(); - if ($optionType == 'field' || $optionType == 'area') { - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => 'test' - ]; - } elseif ($optionType == 'drop_down') { - $optionSelectValues = $customOption->getValues(); - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => reset($optionSelectValues)->getOptionTypeId() - ]; - } elseif ($optionType == 'multiple') { - $customOptionsValues[] = [ - 'id' => (int)$customOption->getOptionId(), - 'value_string' => '[' . implode(',', array_keys($customOption->getValues())) . ']' - ]; + $customOptionsValues[$optionType]['id'] = (int)$customOption->getOptionId(); + switch ($optionType) { + case 'date': + $customOptionsValues[$optionType]['value_string'] = '2012-12-12 00:00:00'; + break; + case 'field': + case 'area': + $customOptionsValues[$optionType]['value_string'] = 'test'; + break; + case 'drop_down': + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[$optionType]['value_string'] = + reset($optionSelectValues)->getOptionTypeId(); + break; + case 'multiple': + $customOptionsValues[$optionType]['value_string'] = + '[' . implode(',', array_keys($customOption->getValues())) . ']'; + break; } } - return $customOptionsValues; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php index 4deed99243f9d..59f11be6d5b45 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php @@ -45,13 +45,43 @@ public function testAddSimpleProductToCart() $response = $this->graphQlMutation($query); self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + self::assertArrayHasKey('shipping_addresses', $response['addSimpleProductsToCart']['cart']); + self::assertEmpty($response['addSimpleProductsToCart']['cart']['shipping_addresses']); self::assertEquals($quantity, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + self::assertArrayHasKey('prices', $response['addSimpleProductsToCart']['cart']['items'][0]); + self::assertArrayHasKey('id', $response['addSimpleProductsToCart']['cart']); + self::assertEquals($maskedQuoteId, $response['addSimpleProductsToCart']['cart']['id']); + + self::assertArrayHasKey('price', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $price = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['price']; + self::assertArrayHasKey('value', $price); + self::assertEquals(10, $price['value']); + self::assertArrayHasKey('currency', $price); + self::assertEquals('USD', $price['currency']); + + self::assertArrayHasKey('row_total', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $rowTotal = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total']; + self::assertArrayHasKey('value', $rowTotal); + self::assertEquals(20, $rowTotal['value']); + self::assertArrayHasKey('currency', $rowTotal); + self::assertEquals('USD', $rowTotal['currency']); + + self::assertArrayHasKey( + 'row_total_including_tax', + $response['addSimpleProductsToCart']['cart']['items'][0]['prices'] + ); + $rowTotalIncludingTax = + $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total_including_tax']; + self::assertArrayHasKey('value', $rowTotalIncludingTax); + self::assertEquals(20, $rowTotalIncludingTax['value']); + self::assertArrayHasKey('currency', $rowTotalIncludingTax); + self::assertEquals('USD', $rowTotalIncludingTax['currency']); } /** * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field AddSimpleProductsToCartInput.cart_id of required type String! was not provided. */ public function testAddSimpleProductToCartIfCartIdIsMissed() { @@ -100,10 +130,6 @@ public function testAddSimpleProductToCartIfCartIdIsEmpty() $this->graphQlMutation($query); } - /** - * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_items" is missing - */ public function testAddSimpleProductToCartIfCartItemsAreMissed() { $query = <<<QUERY @@ -121,6 +147,11 @@ public function testAddSimpleProductToCartIfCartItemsAreMissed() } } QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'Field AddSimpleProductsToCartInput.cart_items of required type [SimpleProductCartItemInput]!' + . ' was not provided.' + ); $this->graphQlMutation($query); } @@ -226,11 +257,40 @@ private function getQuery(string $maskedQuoteId, string $sku, float $quantity): } ) { cart { + id items { quantity product { sku } + prices { + price { + value + currency + } + row_total { + value + currency + } + row_total_including_tax { + value + currency + } + } + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php index 131ff0c480d64..c5723d137d070 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php @@ -45,13 +45,15 @@ public function testAddVirtualProductToCart() $response = $this->graphQlMutation($query); self::assertArrayHasKey('cart', $response['addVirtualProductsToCart']); + self::assertArrayHasKey('id', $response['addVirtualProductsToCart']['cart']); + self::assertEquals($maskedQuoteId, $response['addVirtualProductsToCart']['cart']['id']); self::assertEquals($quantity, $response['addVirtualProductsToCart']['cart']['items'][0]['quantity']); self::assertEquals($sku, $response['addVirtualProductsToCart']['cart']['items'][0]['product']['sku']); } /** * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field AddSimpleProductsToCartInput.cart_id of required type String! was not provided. */ public function testAddVirtualProductToCartIfCartIdIsMissed() { @@ -100,10 +102,6 @@ public function testAddVirtualProductToCartIfCartIdIsEmpty() $this->graphQlMutation($query); } - /** - * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_items" is missing - */ public function testAddVirtualProductToCartIfCartItemsAreMissed() { $query = <<<QUERY @@ -122,6 +120,12 @@ public function testAddVirtualProductToCartIfCartItemsAreMissed() } QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'Field AddSimpleProductsToCartInput.cart_items of required type [SimpleProductCartItemInput]!' + . ' was not provided.' + ); + $this->graphQlMutation($query); } @@ -227,6 +231,7 @@ private function getQuery(string $maskedQuoteId, string $sku, float $quantity): } ) { cart { + id items { quantity product { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php index 16f291be91078..60c3cc2e8b24e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php @@ -104,11 +104,10 @@ public function testSetBillingAddressToGuestCustomerCart() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -217,11 +216,10 @@ public function testSetNewShippingAddressOnCartWithGuestCheckoutDisabled() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AZ" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -318,7 +316,7 @@ private function getQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php index 35ad3ad34c7d6..865837e6bd629 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php @@ -41,6 +41,8 @@ public function testApplyCouponToCart() $response = $this->graphQlMutation($query); self::assertArrayHasKey('applyCouponToCart', $response); + self::assertArrayHasKey('id', $response['applyCouponToCart']['cart']); + self::assertEquals($maskedQuoteId, $response['applyCouponToCart']['cart']['id']); self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); } @@ -182,11 +184,11 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_id' => [ 'coupon_code: "test"', - 'Required parameter "cart_id" is missing' + 'Field ApplyCouponToCartInput.cart_id of required type String! was not provided.' ], 'missed_coupon_code' => [ 'cart_id: "test_quote"', - 'Required parameter "coupon_code" is missing' + 'Field ApplyCouponToCartInput.coupon_code of required type String! was not provided.' ], ]; } @@ -202,6 +204,7 @@ private function getQuery(string $maskedQuoteId, string $couponCode): string mutation { applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { cart { + id applied_coupon { code } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php new file mode 100644 index 0000000000000..7d5e21cd25b8a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php @@ -0,0 +1,192 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test Apply Coupons to Cart functionality for guest + */ +class ApplyCouponsToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponsToCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products. + */ + public function testApplyCouponsToCartWithoutItems() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + */ + public function testApplyCouponsToCustomerCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyNonExistentCouponToCart() + { + $couponCode = 'non_existent_coupon_code'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponsToNonExistentCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('Could not find a cart with ID "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * Products in cart don't fit to the coupon + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyCouponsWhichIsNotApplicable() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @expectedException \Exception + */ + public function testApplyCouponsWithMissedRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {{$input}}) { + cart { + applied_coupons { + code + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'coupon_code: "test"', + 'Field ApplyCouponToCartInput.cart_id of required type String! was not provided.' + ], + 'missed_coupon_code' => [ + 'cart_id: "test_quote"', + 'Field ApplyCouponToCartInput.coupon_code of required type String! was not provided.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function getQuery(string $maskedQuoteId, string $couponCode): string + { + return <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { + cart { + applied_coupons { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php index ed5aa9303d875..315c046148506 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php @@ -95,7 +95,7 @@ private function findProduct(): string products ( filter: { sku: { - like:"simple%" + eq:"simple1" } } pageSize: 1 @@ -219,7 +219,6 @@ private function setBillingAddress(string $cartId): void telephone: "88776655" region: "TX" country_code: "US" - save_in_address_book: false } } } @@ -258,7 +257,6 @@ private function setShippingAddress(string $cartId): array postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -386,7 +384,7 @@ private function placeOrder(string $cartId): void } ) { order { - order_id + order_number } } } @@ -394,8 +392,8 @@ private function placeOrder(string $cartId): void $response = $this->graphQlMutation($query); self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertNotEmpty($response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_number']); } public function tearDown() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php index 81222a84435c8..867aaab7b3a58 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php @@ -67,11 +67,18 @@ public function testGetAvailableShippingMethods() 'value' => 10, 'currency' => 'USD', ], + 'base_amount' => null, ]; self::assertEquals( $expectedAddressData, $response['cart']['shipping_addresses'][0]['available_shipping_methods'][0] ); + self::assertCount(1, $response['cart']['shipping_addresses'][0]['cart_items']); + self::assertCount(1, $response['cart']['shipping_addresses'][0]['cart_items_v2']); + self::assertEquals( + 'simple_product', + $response['cart']['shipping_addresses'][0]['cart_items_v2'][0]['product']['sku'] + ); } /** @@ -135,25 +142,53 @@ private function getQuery(string $maskedQuoteId): string query { cart (cart_id: "{$maskedQuoteId}") { shipping_addresses { - available_shipping_methods { - amount { - value - currency - } - carrier_code - carrier_title - error_message - method_code - method_title - price_excl_tax { - value - currency - } - price_incl_tax { - value - currency - } + cart_items { + cart_item_id + quantity + } + cart_items_v2 { + id + quantity + product { + sku + } + } + available_shipping_methods { + amount { + value + currency + } + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax { + value + currency + } + price_incl_tax { + value + currency + } + base_amount { + value + currency + } + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax { + value + currency + } + price_incl_tax { + value + currency } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartIsVirtualTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartIsVirtualTest.php new file mode 100644 index 0000000000000..79fe2273184b2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartIsVirtualTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting is_virtual from cart + */ +class GetCartIsVirtualTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetMixedCartIsNotVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertFalse($response['cart']['is_virtual']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCartIsVirtual() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('is_virtual', $response['cart']); + $this->assertTrue($response['cart']['is_virtual']); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id:"$maskedQuoteId") { + is_virtual + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php index d39f1b42459c7..ae9b7b32b2dab 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -44,6 +44,8 @@ public function testGetCart() self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('items', $response['cart']); + self::assertArrayHasKey('id', $response['cart']); + self::assertEquals($maskedQuoteId, $response['cart']['id']); self::assertCount(2, $response['cart']['items']); self::assertNotEmpty($response['cart']['items'][0]['id']); @@ -184,6 +186,7 @@ private function getQuery(string $maskedQuoteId): string return <<<QUERY { cart(cart_id: "{$maskedQuoteId}") { + id items { id quantity diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php index fa22786e7acaa..a6e4a4afa9825 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php @@ -91,18 +91,7 @@ public function testGetSelectedShippingMethodBeforeSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - - self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - - self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - - self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** @@ -144,12 +133,7 @@ public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); - self::assertNull($shippingAddress['selected_shipping_method']['amount']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php index 8ddf1641ede8a..cb1565879a81e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php @@ -81,28 +81,7 @@ public function testGetSpecifiedBillingAddressIfBillingAddressIsNotSet() $response = $this->graphQlQuery($query); self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('billing_address', $response['cart']); - - $expectedBillingAddressData = [ - 'firstname' => null, - 'lastname' => null, - 'company' => null, - 'street' => [ - '' - ], - 'city' => null, - 'region' => [ - 'code' => null, - 'label' => null, - ], - 'postcode' => null, - 'country' => [ - 'code' => null, - 'label' => null, - ], - 'telephone' => null, - '__typename' => 'BillingCartAddress', - ]; - self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + self::assertNull($response['cart']['billing_address']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedShippingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedShippingAddressTest.php index f71915bab650f..b5fa0d8f12dfc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedShippingAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedShippingAddressTest.php @@ -82,27 +82,7 @@ public function testGetSpecifiedShippingAddressIfShippingAddressIsNotSet() self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('shipping_addresses', $response['cart']); - $expectedShippingAddressData = [ - 'firstname' => null, - 'lastname' => null, - 'company' => null, - 'street' => [ - '' - ], - 'city' => null, - 'region' => [ - 'code' => null, - 'label' => null, - ], - 'postcode' => null, - 'country' => [ - 'code' => null, - 'label' => null, - ], - 'telephone' => null, - '__typename' => 'ShippingCartAddress', - ]; - self::assertEquals($expectedShippingAddressData, current($response['cart']['shipping_addresses'])); + self::assertEquals([], $response['cart']['shipping_addresses']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/MergeCartsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/MergeCartsTest.php new file mode 100644 index 0000000000000..e558ac41eae52 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/MergeCartsTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for merging guest carts + */ +class MergeCartsTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testMergeGuestCarts() + { + $firstQuote = $this->quoteFactory->create(); + $this->quoteResource->load($firstQuote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + + $secondQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $secondQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + + $firstMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId()); + $secondMaskedId = $this->quoteIdToMaskedId->execute((int)$secondQuote->getId()); + + $query = $this->getCartMergeMutation($firstMaskedId, $secondMaskedId); + $this->graphQlMutation($query); + } + + /** + * Create the mergeCart mutation + * + * @param string $guestQuoteMaskedId + * @param string $customerQuoteMaskedId + * @return string + */ + private function getCartMergeMutation(string $guestQuoteMaskedId, string $customerQuoteMaskedId): string + { + return <<<QUERY +mutation { + mergeCarts( + source_cart_id: "{$guestQuoteMaskedId}" + destination_cart_id: "{$customerQuoteMaskedId}" + ){ + items { + quantity + product { + sku + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index 2dc5b53b31c7a..c6c1d3be99c59 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -79,8 +79,8 @@ public function testPlaceOrder() $response = $this->graphQlMutation($query); self::assertArrayHasKey('placeOrder', $response); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } /** @@ -97,7 +97,7 @@ public function testPlaceOrderIfCartIdIsEmpty() /** * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field PlaceOrderInput.cart_id of required type String! was not provided. */ public function testPlaceOrderIfCartIdIsMissed() { @@ -105,7 +105,7 @@ public function testPlaceOrderIfCartIdIsMissed() mutation { placeOrder(input: {}) { order { - order_id + order_number } } } @@ -304,7 +304,7 @@ private function getQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php index 57a13e2f1bc03..12c3918fcd0ac 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php @@ -63,7 +63,7 @@ public function testRemoveCouponFromCartIfCartIdIsEmpty() /** * @expectedException Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing + * @expectedExceptionMessage Field RemoveCouponFromCartInput.cart_id of required type String! was not provided. */ public function testRemoveCouponFromCartIfCartIdIsMissed() { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php index 7e3d3553ba2f3..c3a66291251c7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php @@ -114,11 +114,11 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_id' => [ 'cart_item_id: 1', - 'Required parameter "cart_id" is missing.' + 'Field RemoveItemFromCartInput.cart_id of required type String! was not provided.' ], 'missed_cart_item_id' => [ 'cart_id: "test_quote"', - 'Required parameter "cart_item_id" is missing.' + 'Field RemoveItemFromCartInput.cart_item_id of required type Int! was not provided.' ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php index 730e65b4ba8aa..87335bd5c96dd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -42,18 +42,18 @@ public function testSetNewBillingAddress() input: { cart_id: "$maskedQuoteId" billing_address: { - address: { + address: { firstname: "test firstname" lastname: "test lastname" company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false - } + } + same_as_shipping: true } } ) { @@ -72,6 +72,20 @@ public function testSetNewBillingAddress() } __typename } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } } } } @@ -83,9 +97,15 @@ public function testSetNewBillingAddress() self::assertArrayHasKey('billing_address', $cartResponse); $billingAddressResponse = $cartResponse['billing_address']; $this->assertNewAddressFields($billingAddressResponse); + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'ShippingCartAddress'); } /** + * Test case for deprecated `use_for_shipping` param. + * * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php @@ -106,11 +126,10 @@ public function testSetNewBillingAddressWithUseForShippingParameter() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } use_for_shipping: true } @@ -182,11 +201,10 @@ public function testSetBillingAddressToCustomerCart() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -259,11 +277,10 @@ public function testSetBillingAddressOnNonExistentCart() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -326,7 +343,7 @@ public function dataProviderSetWithoutRequiredParameters(): array ], 'missed_cart_id' => [ 'billing_address: {}', - 'Required parameter "cart_id" is missing' + 'Field SetBillingAddressOnCartInput.cart_id of required type String! was not provided.' ] ]; } @@ -346,7 +363,7 @@ public function testSetNewBillingAddressWithoutCustomerAddressIdAndAddress() input: { cart_id: "$maskedQuoteId" billing_address: { - use_for_shipping: true + same_as_shipping: true } } ) { @@ -371,7 +388,7 @@ public function testSetNewBillingAddressWithoutCustomerAddressIdAndAddress() * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_multishipping_with_two_shipping_addresses.php */ - public function testSetNewBillingAddressWithUseForShippingAndMultishipping() + public function testSetNewBillingAddressWithSameAsShippingAndMultishipping() { $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -387,13 +404,12 @@ public function testSetNewBillingAddressWithUseForShippingAndMultishipping() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } - use_for_shipping: true + same_as_shipping: true } } ) { @@ -407,7 +423,7 @@ public function testSetNewBillingAddressWithUseForShippingAndMultishipping() QUERY; self::expectExceptionMessage( - 'Using the "use_for_shipping" option with multishipping is not possible.' + 'Using the "same_as_shipping" option with multishipping is not possible.' ); $this->graphQlMutation($query); } @@ -433,11 +449,10 @@ public function testSetNewBillingAddressRedundantStreetLine() company: "test company" street: ["test street 1", "test street 2", "test street 3"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -476,11 +491,10 @@ public function testSetBillingAddressWithLowerCaseCountry() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "us" telephone: "88776655" - save_in_address_book: false } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index 50fd9647d7c54..e38ccf78d420b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -73,7 +73,7 @@ public function testSetPaymentOnCartWithSimpleProduct() self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('order_number', $response['setPaymentMethodAndPlaceOrder']['order']); } /** @@ -111,7 +111,7 @@ public function testSetPaymentOnCartWithVirtualProduct() self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('order_number', $response['setPaymentMethodAndPlaceOrder']['order']); } /** @@ -232,7 +232,7 @@ private function getQuery( } }) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php index fdec9a1dd9853..24ba3e78f9b4e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php @@ -185,11 +185,11 @@ public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array return [ 'missed_cart_id' => [ 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', - 'Required parameter "cart_id" is missing.' + 'Field SetPaymentMethodOnCartInput.cart_id of required type String! was not provided.' ], 'missed_payment_method' => [ 'cart_id: "cart_id_value"', - 'Required parameter "code" for "payment_method" is missing.' + 'Field SetPaymentMethodOnCartInput.payment_method of required type PaymentMethodInput! was not provided' ], 'missed_payment_method_code' => [ 'cart_id: "cart_id_value", payment_method: {code: ""}', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index 537c8f09a0a98..b142de71e89a3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -49,12 +49,12 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } + customer_notes: "Test note" } ] } @@ -73,6 +73,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() label } __typename + customer_notes } } } @@ -112,11 +113,10 @@ public function testSetNewShippingAddressOnCartWithVirtualProduct() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -264,11 +264,10 @@ public function testSetNewShippingAddressOnCartWithRedundantStreetLine() company: "test company" street: ["test street 1", "test street 2", "test street 3"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -303,7 +302,7 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array ], 'missed_cart_id' => [ 'shipping_addresses: {}', - 'Required parameter "cart_id" is missing' + 'Field SetShippingAddressesOnCartInput.cart_id of required type String! was not provided.' ] ]; } @@ -333,11 +332,10 @@ public function testSetMultipleNewShippingAddresses() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } }, { @@ -347,11 +345,10 @@ public function testSetMultipleNewShippingAddresses() company: "test company 2" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -387,11 +384,10 @@ public function testSetShippingAddressOnNonExistentCart() company: "test company" street: ["test street 1", "test street 2"] city: "test city" - region: "test region" + region: "AL" postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -527,7 +523,8 @@ private function assertNewShippingAddressFields(array $shippingAddressResponse): ['response_field' => 'postcode', 'expected_value' => '887766'], ['response_field' => 'telephone', 'expected_value' => '88776655'], ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], - ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'] + ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'], + ['response_field' => 'customer_notes', 'expected_value' => 'Test note'] ]; $this->assertResponseFields($shippingAddressResponse, $assertionMap); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php index 117aedf59b5a5..007ada1ce57cf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -191,11 +191,12 @@ public function dataProviderSetShippingMethodWithWrongParameters(): array carrier_code: "flatrate" method_code: "flatrate" }]', - 'Required parameter "cart_id" is missing' + 'Field SetShippingMethodsOnCartInput.cart_id of required type String! was not provided.' ], 'missed_shipping_methods' => [ 'cart_id: "cart_id_value"', - 'Required parameter "shipping_methods" is missing' + 'Field SetShippingMethodsOnCartInput.shipping_methods of required type [ShippingMethodInput]!' + . ' was not provided.' ], 'shipping_methods_are_empty' => [ 'cart_id: "cart_id_value" shipping_methods: []', @@ -225,7 +226,7 @@ public function dataProviderSetShippingMethodWithWrongParameters(): array 'cart_id: "cart_id_value", shipping_methods: [{ carrier_code: "flatrate" }]', - 'Required parameter "method_code" is missing.' + 'Field ShippingMethodInput.method_code of required type String! was not provided.' ], 'empty_method_code' => [ 'cart_id: "cart_id_value", shipping_methods: [{ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index 988ead7d86df3..48d58a0dd8f17 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -70,6 +70,17 @@ public function testUpdateCartItemQuantity() $this->assertEquals($itemId, $item['id']); $this->assertEquals($quantity, $item['quantity']); + + //Check that update is correctly reflected in cart + $cartQuery = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($cartQuery); + + $this->assertArrayHasKey('cart', $response); + + $responseCart = $response['cart']; + $item = current($responseCart['items']); + + $this->assertEquals($quantity, $item['quantity']); } /** @@ -91,6 +102,15 @@ public function testRemoveCartItemIfQuantityIsZero() $responseCart = $response['updateCartItems']['cart']; $this->assertCount(0, $responseCart['items']); + + //Check that update is correctly reflected in cart + $cartQuery = $this->getCartQuery($maskedQuoteId); + $response = $this->graphQlQuery($cartQuery); + + $this->assertArrayHasKey('cart', $response); + + $responseCart = $response['cart']; + $this->assertCount(0, $responseCart['items']); } /** @@ -163,7 +183,7 @@ public function testUpdateItemFromCustomerCart() /** * @expectedException \Exception - * @expectedExceptionMessage Required parameter "cart_id" is missing. + * @expectedExceptionMessage Field UpdateCartItemsInput.cart_id of required type String! was not provided. */ public function testUpdateWithMissedCartItemId() { @@ -228,11 +248,11 @@ public function dataProviderUpdateWithMissedRequiredParameters(): array return [ 'missed_cart_items' => [ '', - 'Required parameter "cart_items" is missing.' + 'Field UpdateCartItemsInput.cart_items of required type [CartItemUpdateInput]! was not provided.' ], 'missed_cart_item_id' => [ 'cart_items: [{ quantity: 2 }]', - 'Required parameter "cart_item_id" for "cart_items" is missing.' + 'Field CartItemUpdateInput.cart_item_id of required type Int! was not provided.' ], 'missed_cart_item_qty' => [ 'cart_items: [{ cart_item_id: 1 }]', @@ -268,6 +288,26 @@ private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): } } } +QUERY; + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery(string $maskedQuoteId) + { + return <<<QUERY +query { + cart(cart_id: "{$maskedQuoteId}"){ + items{ + product{ + name + } + quantity + } + } +} QUERY; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php index 9c969befa328b..5d1f5847e8419 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\Sales; +use Exception; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\TestFramework\Helper\Bootstrap; @@ -38,9 +39,7 @@ public function testOrdersQuery() query { customerOrders { items { - id - increment_id - created_at + order_number grand_total status } @@ -54,27 +53,27 @@ public function testOrdersQuery() $expectedData = [ [ - 'increment_id' => '100000002', + 'order_number' => '100000002', 'status' => 'processing', 'grand_total' => 120.00 ], [ - 'increment_id' => '100000003', + 'order_number' => '100000003', 'status' => 'processing', 'grand_total' => 130.00 ], [ - 'increment_id' => '100000004', + 'order_number' => '100000004', 'status' => 'closed', 'grand_total' => 140.00 ], [ - 'increment_id' => '100000005', + 'order_number' => '100000005', 'status' => 'complete', 'grand_total' => 150.00 ], [ - 'increment_id' => '100000006', + 'order_number' => '100000006', 'status' => 'complete', 'grand_total' => 160.00 ] @@ -84,23 +83,42 @@ public function testOrdersQuery() foreach ($expectedData as $key => $data) { $this->assertEquals( - $data['increment_id'], - $actualData[$key]['increment_id'], - "increment_id is different than the expected for order - " . $data['increment_id'] + $data['order_number'], + $actualData[$key]['order_number'], + "order_number is different than the expected for order - " . $data['order_number'] ); $this->assertEquals( $data['grand_total'], $actualData[$key]['grand_total'], - "grand_total is different than the expected for order - " . $data['increment_id'] + "grand_total is different than the expected for order - " . $data['order_number'] ); $this->assertEquals( $data['status'], $actualData[$key]['status'], - "status is different than the expected for order - " . $data['increment_id'] + "status is different than the expected for order - " . $data['order_number'] ); } } + /** + * @expectedException Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testOrdersQueryNotAuthorized() + { + $query = <<<QUERY +{ + customerOrders { + items { + increment_id + grand_total + } + } +} +QUERY; + $this->graphQlQuery($query); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 4657a1e763ae1..076c7bece5ff7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -30,6 +30,7 @@ protected function setUp() /** * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default_store store/information/name Test Store */ public function testGetStoreConfig() { @@ -62,7 +63,8 @@ public function testGetStoreConfig() secure_base_url, secure_base_link_url, secure_base_static_url, - secure_base_media_url + secure_base_media_url, + store_name } } QUERY; @@ -89,5 +91,6 @@ public function testGetStoreConfig() $response['storeConfig']['secure_base_static_url'] ); $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $response['storeConfig']['secure_base_media_url']); + $this->assertEquals('Test Store', $response['storeConfig']['store_name']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php index f0397c51c4660..8ba8b534cfe5c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSearchTest.php @@ -30,7 +30,7 @@ public function testFilterLn() products ( filter:{ sku:{ - like:"%simple%" + in:["simple1", "simple2", "simple3"] } } pageSize: 4 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php index dfbe943ecdcd9..1dc5a813de2b8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php @@ -38,6 +38,12 @@ class ProductViewTest extends GraphQlAbstract /** @var \Magento\Tax\Model\Calculation\Rule[] */ private $fixtureTaxRules; + /** @var string */ + private $defaultRegionSystemSetting; + + /** @var string */ + private $defaultPriceDisplayType; + /** * @var StoreManagerInterface */ @@ -52,19 +58,26 @@ protected function setUp() /** @var \Magento\Config\Model\ResourceModel\Config $config */ $config = $this->objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + + $this->defaultRegionSystemSetting = $scopeConfig->getValue( + Config::CONFIG_XML_PATH_DEFAULT_REGION + ); + + $this->defaultPriceDisplayType = $scopeConfig->getValue( + Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE + ); + //default state tax calculation AL $config->saveConfig( Config::CONFIG_XML_PATH_DEFAULT_REGION, - 1, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 1 ); $config->saveConfig( Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, - 3, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + 3 ); $this->getFixtureTaxRates(); $this->getFixtureTaxRules(); @@ -72,6 +85,9 @@ protected function setUp() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $config->reinit(); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } public function tearDown() @@ -82,16 +98,12 @@ public function tearDown() //default state tax calculation AL $config->saveConfig( Config::CONFIG_XML_PATH_DEFAULT_REGION, - null, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + $this->defaultRegionSystemSetting ); $config->saveConfig( Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, - 1, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + $this->defaultPriceDisplayType ); $taxRules = $this->getFixtureTaxRules(); if (count($taxRules)) { @@ -107,6 +119,10 @@ public function tearDown() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $config->reinit(); + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } /** @@ -253,7 +269,23 @@ private function getFixtureTaxRules() */ private function assertBaseFields($product, $actualResponse) { - // ['product_object_field_name', 'expected_value'] + $pricesTypes = [ + 'minimalPrice', + 'regularPrice', + 'maximalPrice', + ]; + foreach ($pricesTypes as $priceType) { + if (isset($actualResponse['price'][$priceType]['amount']['value'])) { + $actualResponse['price'][$priceType]['amount']['value'] = + round($actualResponse['price'][$priceType]['amount']['value'], 4); + } + + if (isset($actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'])) { + $actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'] = + round($actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'], 4); + } + } + // product_object_field_name, expected_value $assertionMap = [ ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], @@ -263,7 +295,7 @@ private function assertBaseFields($product, $actualResponse) [ 'minimalPrice' => [ 'amount' => [ - 'value' => 4.106501, + 'value' => 4.1065, 'currency' => 'USD' ], 'adjustments' => [ @@ -271,7 +303,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.286501, + 'value' => 0.2865, 'currency' => 'USD', ], 'code' => 'TAX', @@ -281,7 +313,7 @@ private function assertBaseFields($product, $actualResponse) ], 'regularPrice' => [ 'amount' => [ - 'value' => 10.750001, + 'value' => 10.7500, 'currency' => 'USD' ], 'adjustments' => [ @@ -289,7 +321,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.750001, + 'value' => 0.7500, 'currency' => 'USD', ], 'code' => 'TAX', @@ -299,7 +331,7 @@ private function assertBaseFields($product, $actualResponse) ], 'maximalPrice' => [ 'amount' => [ - 'value' => 4.106501, + 'value' => 4.1065, 'currency' => 'USD' ], 'adjustments' => [ @@ -307,7 +339,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.286501, + 'value' => 0.2865, 'currency' => 'USD', ], 'code' => 'TAX', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index 8eaf33483531d..5e6415f82b25a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -7,23 +7,15 @@ namespace Magento\GraphQl\UrlRewrite; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\Cms\Helper\Page as PageHelper; -use Magento\Store\Model\ScopeInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\UrlRewrite\Model\UrlRewrite; /** * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. */ class UrlResolverTest extends GraphQlAbstract { - - /** @var ObjectManager */ + /** @var ObjectManager */ private $objectManager; protected function setUp() @@ -31,370 +23,6 @@ protected function setUp() $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); } - /** - * Tests if target_path(relative_url) is resolved for Product entity - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testProductUrlResolver() - { - $productSku = 'p002'; - $urlPath = 'p002.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Tests the use case where relative_url is provided as resolver input in the Query - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testProductUrlWithCanonicalUrlInput() - { - $productSku = 'p002'; - $urlPath = 'p002.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - $product->getUrlKey(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $canonicalPath = $actualUrls->getTargetPath(); - $query - = <<<QUERY -{ - urlResolver(url:"{$canonicalPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Test for category entity - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testCategoryUrlResolver() - { - $productSku = 'p002'; - $urlPath2 = 'cat-1.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath2, - 'store_id' => $storeId - ] - ); - $categoryId = $actualUrls->getEntityId(); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath2}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * @magentoApiDataFixture Magento/Cms/_files/pages.php - */ - public function testCMSPageUrlResolver() - { - /** @var \Magento\Cms\Model\Page $page */ - $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); - $page->load('page100'); - $cmsPageId = $page->getId(); - $requestPath = $page->getIdentifier(); - - /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ - $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); - - /** @param \Magento\Cms\Api\Data\PageInterface $page */ - $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); - $expectedEntityType = CmsPageUrlRewriteGenerator::ENTITY_TYPE; - - $query - = <<<QUERY -{ - urlResolver(url:"{$requestPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertEquals($cmsPageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); - } - - /** - * Tests the use case where the url_key of the existing product is changed - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testProductUrlRewriteResolver() - { - $productSku = 'p002'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - $product->setUrlKey('p002-new')->save(); - $urlPath = $product->getUrlKey() . '.html'; - $this->assertEquals($urlPath, 'p002-new.html'); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Tests if null is returned when an invalid request_path is provided as input to urlResolver - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testInvalidUrlResolverInput() - { - $productSku = 'p002'; - $urlPath = 'p002'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertNull($response['urlResolver']); - } - - /** - * Test for category entity with leading slash - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testCategoryUrlWithLeadingSlash() - { - $productSku = 'p002'; - $urlPath = 'cat-1.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $categoryId = $actualUrls->getEntityId(); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - - $query = <<<QUERY -{ - urlResolver(url:"/{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Test resolution of '/' path to home page - */ - public function testResolveSlash() - { - /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface */ - $scopeConfigInterface = $this->objectManager->get(ScopeConfigInterface::class); - $homePageIdentifier = $scopeConfigInterface->getValue( - PageHelper::XML_PATH_HOME_PAGE, - ScopeInterface::SCOPE_STORE - ); - /** @var \Magento\Cms\Model\Page $page */ - $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); - $page->load($homePageIdentifier); - $homePageId = $page->getId(); - /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ - $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); - /** @param \Magento\Cms\Api\Data\PageInterface $page */ - $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); - $query - = <<<QUERY -{ - urlResolver(url:"/") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); - } - - /** - * Test for custom type which point to the valid product/category/cms page. - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testGetNonExistentUrlRewrite() - { - $urlPath = 'non-exist-product.html'; - /** @var UrlRewrite $urlRewrite */ - $urlRewrite = $this->objectManager->create(UrlRewrite::class); - $urlRewrite->load($urlPath, 'request_path'); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => 1 - ] - ); - $targetPath = $actualUrls->getTargetPath(); - - $query = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals('PRODUCT', $response['urlResolver']['type']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - } - /** * Test for custom type which point to the invalid product/category/cms page. * @@ -411,6 +39,7 @@ public function testNonExistentEntityUrlRewrite() id relative_url type + redirectCode } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php index 7448b165fc234..3221026871bc8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php @@ -33,7 +33,7 @@ public function testQueryObjectVariablesSupport() $query = <<<'QUERY' -query GetProductsQuery($pageSize: Int, $filterInput: ProductFilterInput, $priceSort: SortEnum) { +query GetProductsQuery($pageSize: Int, $filterInput: ProductAttributeFilterInput, $priceSort: SortEnum) { products( pageSize: $pageSize filter: $filterInput @@ -58,8 +58,8 @@ public function testQueryObjectVariablesSupport() 'pageSize' => 1, 'priceSort' => 'ASC', 'filterInput' => [ - 'min_price' => [ - 'gt' => 150, + 'price' => [ + 'from' => 150, ], ], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php new file mode 100644 index 0000000000000..385e3419bbf6a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php @@ -0,0 +1,733 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Weee; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; + +/** + * Test for Product Price With FPT + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + */ +class ProductPriceWithFPTTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** @var string[] $objectManager */ + private $initialConfig; + + /** @var ScopeConfigInterface */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + + /** @var ScopeConfigInterface $scopeConfig */ + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + + $currentSettingsArray = [ + 'tax/display/type', + 'tax/weee/enable', + 'tax/weee/display', + 'tax/defaults/region', + 'tax/weee/apply_vat', + 'tax/calculation/price_includes_tax' + ]; + + foreach ($currentSettingsArray as $configPath) { + $this->initialConfig[$configPath] = $this->scopeConfig->getValue( + $configPath + ); + } + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->writeConfig($this->initialConfig); + } + + /** + * Write configuration for weee + * + * @param array $weeTaxSettings + * @return void + */ + private function writeConfig(array $weeTaxSettings): void + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + $this->scopeConfig->clean(); + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Excluding Tax + * FPT Display setting: Including FPT only + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExcludeTaxAndIncludeFPTOnlySettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExcludeTaxAndIncludeFPTOnly(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceExcludeTaxAndIncludeFPTOnlyProvider settings data provider + * + * @return array + */ + public function catalogPriceExcludeTaxAndIncludeFPTOnlySettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/display/type' => '1', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Excluding Tax + * FPT Display setting: Including FPT and FPT description + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExcludeTaxAndIncludeFPTWithDescriptionSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExcludeTaxAndIncludeFPTWithDescription(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceExcludeTaxAndIncludeFPTWithDescription settings data provider + * + * @return array + */ + public function catalogPriceExcludeTaxAndIncludeFPTWithDescriptionSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/display/type' => '1', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT only + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnlySettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnly(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + + // final price and regular price are the sum of product price, FPT and product tax + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['final_price']['value'], 2)); + + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['final_price']['value'], 2)); + } + + /** + * CatalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnly settings data provider + * + * @return array + */ + public function catalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnlySettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT and FPT description + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescriptionSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescription(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + // final price and regular price are the sum of product price and FPT + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['final_price']['value'], 2)); + + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['final_price']['value'], 2)); + } + + /** + * CatalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescription settings data provider + * + * @return array + */ + public function catalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescriptionSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Including Tax + * Catalog Display setting: Excluding Tax + * FPT Display setting: Including FPT and FPT description + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescriptionSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescription(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescription settings data provider + * + * @return array + */ + public function catalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescriptionSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '1', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '1', + ] + ] + ]; + } + + /** + * Catalog Prices : Including Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT Only + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnlySettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnly(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnly settings data provider + * + * @return array + */ + public function catalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnlySettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Including Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT and FPT Description + * Apply Tax to FPT = Yes + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPTSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPT( + array $weeTaxSettings + ) { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + //12.7 + 7.5% of 12.7 = 13.65 + $fptWithTax = round(13.65, 2); + // final price and regular price are the sum of product price and FPT + $this->assertEquals(113.65, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertEquals(113.65, round($product['price_range']['minimum_price']['final_price']['value'], 2)); + + $this->assertEquals(113.65, round($product['price_range']['maximum_price']['regular_price']['value'], 2)); + $this->assertEquals(113.65, round($product['price_range']['maximum_price']['final_price']['value'], 2)); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals($fptWithTax, round($fixedProductTax['amount']['value'], 2)); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPT settings data provider + * + * @return array + */ + public function catalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPTSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '1', + ] + ] + ]; + } + + /** + * Use multiple FPTs per product with the below tax/fpt configurations + * + * Catalog Prices : Including Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT and FPT description + * Apply tax on FPT : Yes + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_two_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTs(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $product1->setFixedProductAttribute( + [['website_id' => 0, 'country' => 'US', 'state' => 0, 'price' => 10, 'delete' => '']] + ); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertEquals(124.40, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertCount( + 2, + $product['price_range']['minimum_price']['fixed_product_taxes'], + 'Fixed product tax count is incorrect' + ); + $this->assertResponseFields( + $product['price_range']['minimum_price']['fixed_product_taxes'], + [ + [ + 'amount' => [ + 'value' => 13.6525 + ], + 'label' => 'fpt_for_all_front_label' + ], + [ + 'amount' => [ + 'value' => 10.75 + ], + 'label' => 'fixed_product_attribute_front_label' + ], + ] + ); + } + + /** + * CatalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSettingsProvider settings data provider + * + * @return array + */ + public function catalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '1', + ] + ] + ]; + } + + /** + * Test FPT disabled feature + * + * FPT enabled : FALSE + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceDisabledFPTSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + */ + public function testCatalogPriceDisableFPT(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setFixedProductAttribute( + [['website_id' => 0, 'country' => 'US', 'state' => 0, 'price' => 10, 'delete' => '']] + ); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertEquals(100, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertCount( + 0, + $product['price_range']['minimum_price']['fixed_product_taxes'], + 'Fixed product tax count is incorrect' + ); + $this->assertResponseFields( + $product['price_range']['minimum_price']['fixed_product_taxes'], + [] + ); + } + + /** + * CatalogPriceDisableFPT settings data provider + * + * @return array + */ + public function catalogPriceDisabledFPTSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/weee/enable' => '0', + 'tax/weee/display' => '1', + ], + ], + ]; + } + + /** + * Get GraphQl query to fetch products by sku + * + * @param array $skus + * @return string + */ + private function getProductQuery(array $skus): string + { + $stringSkus = '"' . implode('","', $skus) . '"'; + return <<<QUERY +{ + products(filter: {sku: {in: [$stringSkus]}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + fixed_product_taxes{ + amount{value} + label + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + fixed_product_taxes + { + amount{value} + label + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php new file mode 100644 index 0000000000000..451ea78ee308d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Weee; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Weee\Model\Tax as WeeeDisplayConfig; +use Magento\Weee\Model\Config; + +/** + * Test for storeConfig FPT config values + */ +class StoreConfigFPTTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() :void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * FPT All Display settings + * + * @param array $weeTaxSettings + * @param string $displayValue + * @return void + * + * @dataProvider sameFPTDisplaySettingsProvider + */ + public function testSameFPTDisplaySettings(array $weeTaxSettings, $displayValue) + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); + + $query = $this->getStoreConfigQuery(); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + + $this->assertNotEmpty($result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['sales_fixed_product_tax_display_setting']); + + $this->assertEquals($displayValue, $result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertEquals($displayValue, $result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertEquals($displayValue, $result['storeConfig']['sales_fixed_product_tax_display_setting']); + } + + /** + * SameFPTDisplaySettings settings data provider + * + * @return array + */ + public function sameFPTDisplaySettingsProvider() + { + return [ + [ + 'weeTaxSettingsDisplayIncludedOnly' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_INCL, + ], + 'displayValue' => 'INCLUDE_FPT_WITHOUT_DETAILS', + ], + [ + 'weeTaxSettingsDisplayIncludedAndDescription' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + ], + 'displayValue' => 'INCLUDE_FPT_WITH_DETAILS', + ], + [ + 'weeTaxSettingsDisplayIncludedAndExcludedAndDescription' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + ], + 'displayValue' => 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + ], + [ + 'weeTaxSettingsDisplayExcluded' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL, + ], + 'displayValue' => 'EXCLUDE_FPT_WITHOUT_DETAILS', + ], + [ + 'weeTaxSettingsDisplayExcluded' => [ + 'tax/weee/enable' => '0', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL, + ], + 'displayValue' => 'FPT_DISABLED', + ], + ]; + } + + /** + * FPT Display setting shuffled + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider differentFPTDisplaySettingsProvider + */ + public function testDifferentFPTDisplaySettings(array $weeTaxSettings) + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); + + $query = $this->getStoreConfigQuery(); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + + $this->assertNotEmpty($result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['sales_fixed_product_tax_display_setting']); + + $this->assertEquals( + 'INCLUDE_FPT_WITHOUT_DETAILS', + $result['storeConfig']['product_fixed_product_tax_display_setting'] + ); + $this->assertEquals( + 'INCLUDE_FPT_WITH_DETAILS', + $result['storeConfig']['category_fixed_product_tax_display_setting'] + ); + $this->assertEquals( + 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + $result['storeConfig']['sales_fixed_product_tax_display_setting'] + ); + } + + /** + * DifferentFPTDisplaySettings settings data provider + * + * @return array + */ + public function differentFPTDisplaySettingsProvider() + { + return [ + [ + 'weeTaxSettingsDisplay' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + ] + ], + ]; + } + + /** + * Get GraphQl query to fetch storeConfig and FPT serttings + * + * @return string + */ + private function getStoreConfigQuery(): string + { + return <<<QUERY +{ + storeConfig { + product_fixed_product_tax_display_setting + category_fixed_product_tax_display_setting + sales_fixed_product_tax_display_setting + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php new file mode 100644 index 0000000000000..fbd9c53faf7f5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\ResourceModel\Wishlist\CollectionFactory; + +class CustomerWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CollectionFactory + */ + private $wishlistCollectionFactory; + + protected function setUp() + { + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->wishlistCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + /** + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testCustomerWishlist(): void + { + /** @var \Magento\Wishlist\Model\Wishlist $wishlist */ + $collection = $this->wishlistCollectionFactory->create()->filterByCustomerId(1); + + /** @var Item $wishlistItem */ + $wishlistItem = $collection->getFirstItem(); + $query = + <<<QUERY +{ + customer { + wishlist { + id + items_count + sharing_code + updated_at + items { + product { + sku + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + $this->assertEquals((string)$wishlistItem->getId(), $response['customer']['wishlist']['id']); + $this->assertEquals($wishlistItem->getItemsCount(), $response['customer']['wishlist']['items_count']); + $this->assertEquals($wishlistItem->getSharingCode(), $response['customer']['wishlist']['sharing_code']); + $this->assertEquals($wishlistItem->getUpdatedAt(), $response['customer']['wishlist']['updated_at']); + $this->assertEquals('simple', $response['customer']['wishlist']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCustomerAlwaysHasWishlist(): void + { + $query = + <<<QUERY +{ + customer { + wishlist { + id + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + + $this->assertNotEmpty($response['customer']['wishlist']['id']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testGuestCannotGetWishlist() + { + $query = + <<<QUERY +{ + customer { + wishlist { + items_count + sharing_code + updated_at + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php new file mode 100644 index 0000000000000..46844438fdd97 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Api; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Tests web-api for multishipping quote. + */ +class CartRepositoryTest extends WebapiAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + $this->sortOrderBuilder = $this->objectManager->create(SortOrderBuilder::class); + $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + try { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $cart = $this->getCart('multishipping_quote_id'); + $quoteRepository->delete($cart); + } catch (\InvalidArgumentException $e) { + // Do nothing if cart fixture was not used + } + parent::tearDown(); + } + + /** + * Tests that multishipping quote contains all addresses in shipping assignments. + * + * @magentoApiDataFixture Magento/Multishipping/Fixtures/quote_with_split_items.php + */ + public function testGetMultishippingCart() + { + $cart = $this->getCart('multishipping_quote_id'); + $cartId = $cart->getId(); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/carts/' . $cartId, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => 'quoteCartRepositoryV1', + 'serviceVersion' => 'V1', + 'operation' => 'quoteCartRepositoryV1Get', + ], + ]; + + $requestData = ['cartId' => $cartId]; + $cartData = $this->_webApiCall($serviceInfo, $requestData); + + $shippingAssignments = $cart->getExtensionAttributes()->getShippingAssignments(); + foreach ($shippingAssignments as $key => $shippingAssignment) { + $address = $shippingAssignment->getShipping()->getAddress(); + $cartItem = $shippingAssignment->getItems()[0]; + $this->assertEquals( + $address->getId(), + $cartData['extension_attributes']['shipping_assignments'][$key]['shipping']['address']['id'] + ); + $this->assertEquals( + $cartItem->getSku(), + $cartData['extension_attributes']['shipping_assignments'][$key]['items'][0]['sku'] + ); + $this->assertEquals( + $cartItem->getQty(), + $cartData['extension_attributes']['shipping_assignments'][$key]['items'][0]['qty'] + ); + } + } + + /** + * Retrieve quote by given reserved order ID + * + * @param string $reservedOrderId + * @return Quote + * @throws \InvalidArgumentException + */ + private function getCart(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + if (empty($items)) { + throw new \InvalidArgumentException('There is no quote with provided reserved order ID.'); + } + + return array_pop($items); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php index 6d585561ae3a9..08821b08ede5e 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php @@ -310,21 +310,20 @@ public function testAssignCustomerThrowsExceptionIfCartIsAssignedToDifferentStor } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php * @magentoApiDataFixture Magento/Sales/_files/quote.php - * @expectedException \Exception */ - public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart() + public function testAssignCustomerCartMerged() { /** @var $customer \Magento\Customer\Model\Customer */ $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class)->load(1); // Customer has a quote with reserved order ID test_order_1 (see fixture) /** @var $customerQuote \Magento\Quote\Model\Quote */ $customerQuote = $this->objectManager->create(\Magento\Quote\Model\Quote::class) - ->load('test_order_1', 'reserved_order_id'); - $customerQuote->setIsActive(1)->save(); + ->load('test_order_item_with_items', 'reserved_order_id'); /** @var $quote \Magento\Quote\Model\Quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); + $expectedQuoteItemsQty = $customerQuote->getItemsQty() + $quote->getItemsQty(); $cartId = $quote->getId(); $customerId = $customer->getId(); @@ -346,11 +345,13 @@ public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart( 'customerId' => $customerId, 'storeId' => 1, ]; - $this->_webApiCall($serviceInfo, $requestData); + $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); - $this->expectExceptionMessage( - "The customer can't be assigned to the cart because the customer already has an active cart." - ); + $mergedQuote = $this->objectManager + ->create(\Magento\Quote\Model\Quote::class) + ->load('test01', 'reserved_order_id'); + + $this->assertEquals($expectedQuoteItemsQty, $mergedQuote->getItemsQty()); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php index 8cb82f5c8f206..5a894758dc9ed 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php @@ -42,6 +42,9 @@ class CartRepositoryTest extends WebapiAbstract */ private $filterBuilder; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -59,8 +62,10 @@ protected function setUp() protected function tearDown() { try { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); $cart = $this->getCart('test01'); - $cart->delete(); + $quoteRepository->delete($cart); } catch (\InvalidArgumentException $e) { // Do nothing if cart fixture was not used } @@ -74,18 +79,27 @@ protected function tearDown() * @return \Magento\Quote\Model\Quote * @throws \InvalidArgumentException */ - protected function getCart($reservedOrderId) + private function getCart($reservedOrderId) { - /** @var $cart \Magento\Quote\Model\Quote */ - $cart = $this->objectManager->get(\Magento\Quote\Model\Quote::class); - $cart->load($reservedOrderId, 'reserved_order_id'); - if (!$cart->getId()) { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + if (empty($items)) { throw new \InvalidArgumentException('There is no quote with provided reserved order ID.'); } - return $cart; + + return array_pop($items); } /** + * Tests successfull get cart web-api call. + * * @magentoApiDataFixture Magento/Sales/_files/quote.php */ public function testGetCart() @@ -130,6 +144,8 @@ public function testGetCart() } /** + * Tests exception when cartId is not provided. + * * @expectedException \Exception * @expectedExceptionMessage No such entity with */ @@ -154,6 +170,8 @@ public function testGetCartThrowsExceptionIfThereIsNoCartWithProvidedId() } /** + * Tests carts search. + * * @magentoApiDataFixture Magento/Sales/_files/quote.php */ public function testGetList() @@ -184,6 +202,7 @@ public function testGetList() $this->searchCriteriaBuilder->addFilters([$grandTotalFilter, $subtotalFilter]); $this->searchCriteriaBuilder->addFilters([$minCreatedAtFilter]); $this->searchCriteriaBuilder->addFilters([$maxCreatedAtFilter]); + $this->searchCriteriaBuilder->addFilter('reserved_order_id', 'test01'); /** @var SortOrder $sortOrder */ $sortOrder = $this->sortOrderBuilder->setField('subtotal')->setDirection(SortOrder::SORT_ASC)->create(); $this->searchCriteriaBuilder->setSortOrders([$sortOrder]); diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php index bbd1e59f83f90..120781e674d47 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php @@ -231,21 +231,20 @@ public function testAssignCustomerThrowsExceptionIfTargetCartIsNotAnonymous() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php * @magentoApiDataFixture Magento/Sales/_files/quote.php - * @expectedException \Exception */ - public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart() + public function testAssignCustomerCartMerged() { /** @var $customer \Magento\Customer\Model\Customer */ $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class)->load(1); // Customer has a quote with reserved order ID test_order_1 (see fixture) /** @var $customerQuote \Magento\Quote\Model\Quote */ $customerQuote = $this->objectManager->create(\Magento\Quote\Model\Quote::class) - ->load('test_order_1', 'reserved_order_id'); - $customerQuote->setIsActive(1)->save(); + ->load('test_order_item_with_items', 'reserved_order_id'); /** @var $quote \Magento\Quote\Model\Quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); + $expectedQuoteItemsQty = $customerQuote->getItemsQty() + $quote->getItemsQty(); $cartId = $quote->getId(); @@ -284,11 +283,12 @@ public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart( 'customerId' => $customerId, 'storeId' => 1, ]; - $this->_webApiCall($serviceInfo, $requestData); + $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); + $mergedQuote = $this->objectManager + ->create(\Magento\Quote\Model\Quote::class) + ->load('test01', 'reserved_order_id'); - $this->expectExceptionMessage( - "The customer can't be assigned to the cart because the customer already has an active cart." - ); + $this->assertEquals($expectedQuoteItemsQty, $mergedQuote->getItemsQty()); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentAddTrackTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentAddTrackTest.php index 054f91a295fd9..639adb8da4624 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentAddTrackTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentAddTrackTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; use Magento\Framework\Webapi\Rest\Request; @@ -18,17 +20,17 @@ class ShipmentAddTrackTest extends WebapiAbstract { /** - * Service read name + * Read name of service */ const SERVICE_READ_NAME = 'salesShipmentTrackRepositoryV1'; /** - * Service version + * Version of service */ const SERVICE_VERSION = 'V1'; /** - * Shipment increment id + * Increment id for shipment */ const SHIPMENT_INCREMENT_ID = '100000001'; @@ -74,6 +76,52 @@ public function testShipmentAddTrack() self::assertNotEmpty($result[ShipmentTrackInterface::ENTITY_ID]); self::assertEquals($shipment->getId(), $result[ShipmentTrackInterface::PARENT_ID]); } + + /** + * Shipment Tracking throw an error if order doesn't exist. + * + * @magentoApiDataFixture Magento/Sales/_files/shipment.php + * @magentoApiDataFixture Magento/Sales/_files/order_list.php + */ + public function testShipmentTrackWithFailedOrderId() + { + /** @var \Magento\Sales\Model\Order $order */ + $orderCollection = $this->objectManager->get(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $order = $orderCollection->getLastItem(); + // Order ID from Magento/Sales/_files/order_list.php + $failedOrderId = $order->getId(); + $shipmentCollection = $this->objectManager->get(Collection::class); + /** @var \Magento\Sales\Model\Order\Shipment $shipment */ + $shipment = $shipmentCollection->getFirstItem(); + $trackData = [ + ShipmentTrackInterface::ENTITY_ID => null, + ShipmentTrackInterface::ORDER_ID => $failedOrderId, + ShipmentTrackInterface::PARENT_ID => $shipment->getId(), + ShipmentTrackInterface::WEIGHT => 20, + ShipmentTrackInterface::QTY => 5, + ShipmentTrackInterface::TRACK_NUMBER => 2, + ShipmentTrackInterface::DESCRIPTION => 'Shipment description', + ShipmentTrackInterface::TITLE => 'Shipment title', + ShipmentTrackInterface::CARRIER_CODE => Track::CUSTOM_CARRIER_CODE, + ]; + $exceptionMessage = ''; + + try { + $this->_webApiCall($this->getServiceInfo(), ['entity' => $trackData]); + } catch (\SoapFault $e) { + $exceptionMessage = $e->getMessage(); + } catch (\Exception $e) { + $errorObj = $this->processRestExceptionResult($e); + $exceptionMessage = $errorObj['message']; + } + + $this->assertContains( + $exceptionMessage, + 'Could not save the shipment tracking.', + 'SoapFault or CouldNotSaveException does not contain exception message.' + ); + } + /** * Returns details about API endpoints and services. * diff --git a/dev/tests/api-functional/testsuite/Magento/Swatches/Api/ProductAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Swatches/Api/ProductAttributeOptionManagementInterfaceTest.php new file mode 100644 index 0000000000000..5cbaa76631c23 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Swatches/Api/ProductAttributeOptionManagementInterfaceTest.php @@ -0,0 +1,225 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Swatches\Api; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Api\Data\AttributeOptionLabelInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Swatches\Model\ResourceModel\Swatch\Collection; +use Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory; +use Magento\Swatches\Model\Swatch; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Test product attribute option management API for swatch attribute type + */ +class ProductAttributeOptionManagementInterfaceTest extends WebapiAbstract +{ + private const ATTRIBUTE_CODE = 'select_attribute'; + private const SERVICE_NAME = 'catalogProductAttributeOptionManagementV1'; + private const SERVICE_VERSION = 'V1'; + private const RESOURCE_PATH = '/V1/products/attributes'; + + /** + * Test add option to swatch attribute + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + * @param array $data + * @param array $payload + * @param string $expectedSwatchType + * @param string $expectedLabel + * @param string $expectedValue + * + * @dataProvider addDataProvider + */ + public function testAdd( + array $data, + array $payload, + string $expectedSwatchType, + string $expectedLabel, + string $expectedValue + ) { + $objectManager = Bootstrap::getObjectManager(); + /** @var $attributeRepository AttributeRepository */ + $attributeRepository = $objectManager->get(AttributeRepository::class); + /** @var $attribute Attribute */ + $attribute = $attributeRepository->get(ProductAttributeInterface::ENTITY_TYPE_CODE, self::ATTRIBUTE_CODE); + $attribute->addData($data); + $attributeRepository->save($attribute); + $response = $this->_webApiCall( + [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . self::ATTRIBUTE_CODE . '/options', + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'add', + ], + ], + [ + 'attributeCode' => self::ATTRIBUTE_CODE, + 'option' => $payload, + ] + ); + + $this->assertNotNull($response); + $optionId = (int) ltrim($response, 'id_'); + $swatch = $this->getSwatch($optionId); + $this->assertEquals($expectedValue, $swatch->getValue()); + $this->assertEquals($expectedSwatchType, $swatch->getType()); + $options = $attribute->setStoreId(0)->getOptions(); + $this->assertCount(3, $options); + $this->assertEquals($expectedLabel, $options[2]->getLabel()); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function addDataProvider() + { + return [ + 'visual swatch option with value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Black', + AttributeOptionInterface::VALUE => '#000000', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Noir', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_VISUAL_COLOR, + 'expectedLabel' => 'Black', + 'expectedValue' => '#000000', + ], + 'visual swatch option without value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Black', + AttributeOptionInterface::VALUE => '', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Noir', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_EMPTY, + 'expectedLabel' => 'Black', + 'expectedValue' => '', + ], + 'text swatch option with value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_TEXT, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Small', + AttributeOptionInterface::VALUE => 'S', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Petit', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_TEXTUAL, + 'expectedLabel' => 'Small', + 'expectedValue' => 'S', + ], + 'text swatch option without value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_TEXT, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Small', + AttributeOptionInterface::VALUE => '', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Petit', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_TEXTUAL, + 'expectedLabel' => 'Small', + 'expectedValue' => '', + ], + 'text swatch option with value - redeclare store ID 0 in store_labels' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_TEXT, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Small', + AttributeOptionInterface::VALUE => 'S', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Slim', + AttributeOptionLabelInterface::STORE_ID => 0, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_TEXTUAL, + 'expectedLabel' => 'Slim', + 'expectedValue' => 'S', + ], + ]; + } + + /** + * Get swatch model + * + * @param int $optionId + * @return Swatch + */ + private function getSwatch(int $optionId) + { + /** @var Collection $collection */ + $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)->create(); + $collection->addFieldToFilter('option_id', $optionId); + $collection->setPageSize(1); + return $collection->getFirstItem(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php index b1f86739786c2..37cb2317b5b65 100644 --- a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php @@ -8,7 +8,7 @@ namespace Magento\WebapiAsync\Model; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\TestFramework\MessageQueue\PreconditionFailedException; use Magento\TestFramework\MessageQueue\PublisherConsumerController; use Magento\TestFramework\MessageQueue\EnvironmentPreconditionException; @@ -19,7 +19,6 @@ use Magento\Framework\Registry; use Magento\Framework\Webapi\Exception; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Model\Store; use Magento\Framework\Webapi\Rest\Request; @@ -128,6 +127,13 @@ protected function setUp() */ public function testAsyncScheduleBulkMultistore($storeCode) { + if ($storeCode === self::STORE_CODE_FROM_FIXTURE) { + /** @var \Magento\Config\Model\Config $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Config\Model\Config::class); + if (strpos($config->getConfigDataValue('catalog/search/engine'), 'elasticsearch') !== false) { + $this->markTestSkipped('MC-20452'); + } + } $product = $this->getProductData(); $this->_markTestAsRestOnly(); @@ -277,9 +283,9 @@ public function getProductData() 'product' => $productBuilder( [ - ProductInterface::TYPE_ID => 'simple', - ProductInterface::SKU => 'multistore-sku-test-1', - ProductInterface::NAME => 'Test Name ', + Product::TYPE_ID => 'simple', + Product::SKU => 'multistore-sku-test-1', + Product::NAME => 'Test Name ', ] ), ]; @@ -303,16 +309,16 @@ public function storeProvider() private function getSimpleProductData($productData = []) { return [ - ProductInterface::SKU => isset($productData[ProductInterface::SKU]) - ? $productData[ProductInterface::SKU] : uniqid('sku-', true), - ProductInterface::NAME => isset($productData[ProductInterface::NAME]) - ? $productData[ProductInterface::NAME] : uniqid('sku-', true), - ProductInterface::VISIBILITY => 4, - ProductInterface::TYPE_ID => 'simple', - ProductInterface::PRICE => 3.62, - ProductInterface::STATUS => 1, - ProductInterface::TYPE_ID => 'simple', - ProductInterface::ATTRIBUTE_SET_ID => 4, + Product::SKU => isset($productData[Product::SKU]) + ? $productData[Product::SKU] : uniqid('sku-', true), + Product::NAME => isset($productData[Product::NAME]) + ? $productData[Product::NAME] : uniqid('sku-', true), + Product::VISIBILITY => 4, + Product::TYPE_ID => 'simple', + Product::PRICE => 3.62, + Product::STATUS => 1, + Product::TYPE_ID => 'simple', + Product::ATTRIBUTE_SET_ID => 4, ]; } diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php index 49f2577b26211..bc3ae83643d3e 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php @@ -9,7 +9,6 @@ use Magento\Mtf\Client\Locator; /** - * Class LiselectstoreElement * Typified element class for lists selectors */ class LiselectstoreElement extends SimpleElement @@ -76,6 +75,7 @@ public function setValue($value) $option = $this->context->find($optionSelector, Locator::SELECTOR_XPATH); if (!$option->isVisible()) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('[' . implode('/', $value) . '] option is not visible in store switcher.'); } $option->click(); @@ -133,7 +133,7 @@ public function getValues() */ protected function isSubstring($haystack, $pattern) { - return preg_match("/$pattern/", $haystack) != 0 ? true : false; + return preg_match("/$pattern/", $haystack) != 0; } /** @@ -157,8 +157,8 @@ protected function findNearestElement($criteria, $key, array $elements) /** * Get selected store value * - * @throws \Exception * @return string + * @throws \Exception */ public function getValue() { diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/EnvWhitelist.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/EnvWhitelist.php new file mode 100644 index 0000000000000..294cc32e8f712 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/Cli/EnvWhitelist.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Mtf\Util\Command\Cli; + +use Magento\Mtf\Util\Command\Cli; + +/** + * Adding and removing domain to whitelist for test execution. + */ +class EnvWhitelist extends Cli +{ + /** + * Parameter domain add command. + */ + const PARAM_DOMAINS = 'downloadable:domains'; + + /** + * Add host to the whitelist. + * + * @param string $host + */ + public function addHost($host) + { + parent::execute(EnvWhitelist::PARAM_DOMAINS . ':add ' . $host); + } + + /** + * Remove host from the whitelist. + * + * @param string $host + */ + public function removeHost($host) + { + parent::execute(EnvWhitelist::PARAM_DOMAINS . ':remove ' . $host); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Form/AuthorizenetCc.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Form/AuthorizenetCc.php deleted file mode 100644 index 3cae648602531..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Form/AuthorizenetCc.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Authorizenet\Test\Block\Form; - -use Magento\Mtf\Client\Locator; -use Magento\Payment\Test\Block\Form\PaymentCc; - -/** - * Form for credit card data for Authorize.net payment method. - */ -class AuthorizenetCc extends PaymentCc -{ - /** - * Authorizenet form locators. - * - * @var array - */ - private $authorizenetForm = [ - "cc_number" => "//*[@id='authorizenet_directpost_cc_number']", - "cc_exp_month" => "//*[@id='authorizenet_directpost_expiration']", - "cc_exp_year" => "//*[@id='authorizenet_directpost_expiration_yr']", - "cc_cid" => "//*[@id='authorizenet_directpost_cc_cid']", - ]; - - /** - * Get Filled CC Number. - * - * @return string - */ - public function getCCNumber() - { - return $this->_rootElement->find($this->authorizenetForm['cc_number'], Locator::SELECTOR_XPATH)->getValue(); - } - - /** - * Get Filled CC Number. - * - * @return string - */ - public function getExpMonth() - { - return $this->_rootElement->find($this->authorizenetForm['cc_exp_month'], Locator::SELECTOR_XPATH)->getValue(); - } - - /** - * Get Expiration Year - * - * @return string - */ - public function getExpYear() - { - return $this->_rootElement->find($this->authorizenetForm['cc_exp_year'], Locator::SELECTOR_XPATH)->getValue(); - } - - /** - * Get CID - * - * @return string - */ - public function getCid() - { - return $this->_rootElement->find($this->authorizenetForm['cc_cid'], Locator::SELECTOR_XPATH)->getValue(); - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Form/AuthorizenetCc.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Form/AuthorizenetCc.xml deleted file mode 100644 index dbb9b4707f1e7..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Form/AuthorizenetCc.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" ?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<mapping strict="0"> - <fields> - <cc_number> - <selector>#authorizenet_directpost_cc_number</selector> - </cc_number> - <cc_exp_month> - <selector>#authorizenet_directpost_expiration</selector> - </cc_exp_month> - <cc_exp_year> - <selector>#authorizenet_directpost_expiration_yr</selector> - </cc_exp_year> - <cc_cid> - <selector>#authorizenet_directpost_cc_cid</selector> - </cc_cid> - </fields> -</mapping> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/AuthorizenetLogin.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/AuthorizenetLogin.php deleted file mode 100644 index 236237361d61e..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/AuthorizenetLogin.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\Block\Sandbox; - -use Magento\Mtf\Block\Form; -use Magento\Mtf\Client\Element\SimpleElement; -use Magento\Mtf\Fixture\FixtureInterface; - -/** - * Login form. - */ -class AuthorizenetLogin extends Form -{ - /** - * Login button on Authorize.Net Sandbox. - * - * @var string - */ - private $loginButton = '[type=submit]'; - - /** - * Switch to the form frame and fill form. {@inheritdoc} - * - * @param FixtureInterface $fixture - * @param SimpleElement|null $element - * @return $this - */ - public function fill(FixtureInterface $fixture, SimpleElement $element = null) - { - parent::fill($fixture, $element); - return $this; - } - - /** - * Login to Authorize.Net Sandbox. - * - * @return void - */ - public function login() - { - $this->_rootElement->find($this->loginButton)->click(); - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/AuthorizenetLogin.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/AuthorizenetLogin.xml deleted file mode 100644 index 81159d8cf8451..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/AuthorizenetLogin.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" ?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<mapping strict="1"> - <fields> - <login_id> - <selector>[name=MerchantLogin]</selector> - </login_id> - <password> - <selector>[name=Password]</selector> - </password> - </fields> -</mapping> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/GetStartedModal.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/GetStartedModal.php deleted file mode 100644 index f7efb023b5799..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/GetStartedModal.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\Block\Sandbox; - -use Magento\Mtf\Block\Block; - -/** - * 'Get started accepting payments' modal window on Authorize.Net sandbox. - */ -class GetStartedModal extends Block -{ - /** - * 'Got It' button selector. - * This button is located in notification window which may appear immediately after login. - * - * @var string - */ - private $gotItButton = '#btnGetStartedGotIt'; - - /** - * Accept notification if it appears after login. - * - * @return $this - */ - public function acceptNotification() - { - $element = $this->browser->find($this->gotItButton); - if ($element->isVisible()) { - $element->click(); - } - return $this; - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/Menu.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/Menu.php deleted file mode 100644 index e4cdc5b8d1a29..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/Menu.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\Block\Sandbox; - -use Magento\Mtf\Block\Block; -use Magento\Mtf\Client\Locator; - -/** - * Menu block on Authorize.Net sandbox. - */ -class Menu extends Block -{ - /** - * Search menu button selector. - * - * @var string - */ - private $searchMenuButton = './/div[@id="topNav"]//a[contains(@href,"search")]'; - - /** - * Open 'Search' menu item. - * - * @return $this - */ - public function openSearchMenu() - { - $this->_rootElement->find($this->searchMenuButton, Locator::SELECTOR_XPATH)->click(); - return $this; - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/SearchForm.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/SearchForm.php deleted file mode 100644 index cceda81e61a36..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/SearchForm.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\Block\Sandbox; - -use Magento\Mtf\Block\Form; - -/** - * Transactions search form. - */ -class SearchForm extends Form -{ - /** - * Search button selector. - * - * @var string - */ - private $searchButton = '[type=submit]'; - - /** - * Search for transactions. - * - * @return void - */ - public function search() - { - $this->browser->find($this->searchButton)->click(); - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/SearchForm.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/SearchForm.xml deleted file mode 100644 index 993d3b4fda748..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/SearchForm.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" ?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<mapping strict="1"> - <fields> - <settlement_date_from> - <selector>[name=StartBatch]</selector> - <input>select</input> - </settlement_date_from> - <settlement_date_to> - <selector>[name=EndBatch]</selector> - <input>select</input> - </settlement_date_to> - </fields> -</mapping> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/TransactionsGrid.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/TransactionsGrid.php deleted file mode 100644 index c9b6ab3b8152c..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Block/Sandbox/TransactionsGrid.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\Block\Sandbox; - -use Magento\Mtf\Block\Block; -use Magento\Mtf\Client\Locator; - -/** - * Transactions grid block. - */ -class TransactionsGrid extends Block -{ - /** - * Transaction selector. - * - * @var string - */ - private $transaction = './/a[contains(text(), "%s")]'; - - /** - * 'Approve' button selector. - * - * @var string - */ - private $transactionApprove = '(//input[@id="btnConfirmApprove"])[1]'; - - /** - * Confirmation window 'OK' button selector. - * - * @var string - */ - private $transactionApprovalConfirm = '#btnConfirmYes'; - - /** - * Find transaction in grid and open it. - * - * @param string $transactionId - * @return $this - */ - public function openTransaction($transactionId) - { - $this->_rootElement->find(sprintf($this->transaction, $transactionId), Locator::SELECTOR_XPATH)->click(); - return $this; - } - - /** - * Approve selected transaction. - * - * @return $this - */ - public function approveTransaction() - { - $this->_rootElement->find($this->transactionApprove, Locator::SELECTOR_XPATH)->click(); - $this->confirmTransactionApproval(); - return $this; - } - - /** - * Confirm approval of selected transaction. - * - * @return void - */ - private function confirmTransactionApproval() - { - $this->browser->find($this->transactionApprovalConfirm)->click(); - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Constraint/AssertCreditCardNumberOnOnePageCheckout.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Constraint/AssertCreditCardNumberOnOnePageCheckout.php deleted file mode 100644 index cd1dfce621806..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Constraint/AssertCreditCardNumberOnOnePageCheckout.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Authorizenet\Test\Constraint; - -use Magento\Checkout\Test\Page\CheckoutOnepage; -use Magento\Mtf\Constraint\AbstractConstraint; -use Magento\Payment\Test\Fixture\CreditCard; - -/** - * Assert credit card fields have set values from fixture. - */ -class AssertCreditCardNumberOnOnePageCheckout extends AbstractConstraint -{ - /** - * Assert payment form values did persist from fixture after checkout blocks refresh - * - * @param CheckoutOnepage $checkoutOnepage - * @param CreditCard $creditCard - * @return void - */ - public function processAssert(CheckoutOnepage $checkoutOnepage, CreditCard $creditCard) - { - \PHPUnit\Framework\Assert::assertEquals( - $creditCard->getCcNumber(), - $checkoutOnepage->getAuthorizenetBlock()->getCCNumber(), - 'Credit card data did persist with the values from fixture' - ); - } - - /** - * Returns string representation of successful assertion - * - * @return string - */ - public function toString() - { - return 'Credit card data did persist with the values from fixture.'; - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Fixture/AuthorizenetSandboxCustomer.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Fixture/AuthorizenetSandboxCustomer.xml deleted file mode 100644 index 89e1aca8c3b78..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Fixture/AuthorizenetSandboxCustomer.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/fixture.xsd"> - <fixture name="authorizenet_sandbox_customer" - module="Magento_Authorizenet" - type="virtual" - repository_class="Magento\Authorizenet\Test\Repository\AuthorizenetSandboxCustomer" - class="Magento\Authorizenet\Test\Fixture\AuthorizenetSandboxCustomer"> - <field name="login_id" /> - <field name="password" /> - </fixture> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Fixture/TransactionSearch.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Fixture/TransactionSearch.xml deleted file mode 100644 index 6d644d2bd3996..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Fixture/TransactionSearch.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/fixture.xsd"> - <fixture name="authorizenet_trasnsaction_search" - module="Magento_Authorizenet" - type="virtual" - repository_class="Magento\Authorizenet\Test\Repository\TransactionSearch" - class="Magento\Authorizenet\Test\Fixture\TransactionSearch"> - <field name="settlement_date_from" /> - <field name="settlement_date_to" /> - </fixture> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Page/CheckoutOnepage.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Page/CheckoutOnepage.xml deleted file mode 100644 index 34e8a1eea62f3..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Page/CheckoutOnepage.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> - <page name="CheckoutOnepage" mca="checkout/index"> - <block name="authorizenetBlock" class="Magento\Authorizenet\Test\Block\Form\AuthorizenetCc" locator="#payment_form_authorizenet_directpost" strategy="css selector"/> - <block name="paymentBlock"> - <render name="authorizenet" class="Magento\Authorizenet\Test\Block\Form\AuthorizenetCc" /> - </block> - </page> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Page/Sandbox/Main.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Page/Sandbox/Main.xml deleted file mode 100644 index da9fd5df60ceb..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Page/Sandbox/Main.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/pages.xsd"> - <page name="Main" area="Sandbox" mca="https://sandbox.authorize.net/" module="Magento_Authorizenet"> - <block name="loginForm" class="Magento\Authorizenet\Test\Block\Sandbox\AuthorizenetLogin" locator="#ctl00_MainContent_welcomeText" strategy="css selector" /> - <block name="menuBlock" class="Magento\Authorizenet\Test\Block\Sandbox\Menu" locator="body" strategy="css selector" /> - <block name="modalBlock" class="Magento\Authorizenet\Test\Block\Sandbox\GetStartedModal" locator="body" strategy="css selector" /> - <block name="searchForm" class="Magento\Authorizenet\Test\Block\Sandbox\SearchForm" locator="#Main" strategy="css selector" /> - <block name="transactionsGridBlock" class="Magento\Authorizenet\Test\Block\Sandbox\TransactionsGrid" locator="#Main" strategy="css selector" /> - </page> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/AuthorizenetSandboxCustomer.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/AuthorizenetSandboxCustomer.xml deleted file mode 100644 index 454da0ef6cecd..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/AuthorizenetSandboxCustomer.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?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="../../../../../../vendor/magento/mtf/Magento/Mtf/Repository/etc/repository.xsd"> - <repository class="Magento\Authorizenet\Test\Repository\AuthorizenetSandboxCustomer"> - <dataset name="sandbox_fraud_hold_review"> - <field name="login_id" xsi:type="string">AUTHORIZENET_SANDBOX_LOGIN_ID</field> - <field name="password" xsi:type="string">AUTHORIZENET_SANDBOX_PASSWORD</field> - </dataset> - </repository> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/ConfigData.xml deleted file mode 100644 index c759b537a191d..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/ConfigData.xml +++ /dev/null @@ -1,163 +0,0 @@ -<?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="../../../../../../vendor/magento/mtf/Magento/Mtf/Repository/etc/repository.xsd"> - <repository class="Magento\Config\Test\Repository\ConfigData"> - <dataset name="authorizenet"> - <field name="payment/authorizenet_directpost/active" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - <field name="payment/authorizenet_directpost/login" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">%payment_authorizenet_login%</item> - </field> - <field name="payment/authorizenet_directpost/trans_key" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">%payment_authorizenet_trans_key%</item> - </field> - <field name="payment/authorizenet_directpost/trans_md5" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">%payment_authorizenet_trans_md5%</item> - </field> - <field name="payment/authorizenet_directpost/test" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">No</item> - <item name="value" xsi:type="number">0</item> - </field> - <field name="payment/authorizenet_directpost/cgi_url" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">https://test.authorize.net/gateway/transact.dll</item> - </field> - <field name="payment/authorizenet_directpost/cgi_url_td" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">https://apitest.authorize.net/xml/v1/request.api</item> - </field> - <field name="payment/authorizenet_directpost/debug" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - <field name="payment/authorizenet_directpost/useccv" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - </dataset> - <dataset name="authorizenet_rollback"> - <field name="payment/authorizenet_directpost/active" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">No</item> - <item name="value" xsi:type="number">0</item> - </field> - </dataset> - <dataset name="authorizenet_fraud_review"> - <field name="payment/authorizenet_directpost/active" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - <field name="payment/authorizenet_directpost/login" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">%authorizenet_fraud_review_login%</item> - </field> - <field name="payment/authorizenet_directpost/trans_key" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">%authorizenet_fraud_review_trans_key%</item> - </field> - <field name="payment/authorizenet_directpost/trans_md5" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">%authorizenet_fraud_review_md5%</item> - </field> - <field name="payment/authorizenet_directpost/test" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">No</item> - <item name="value" xsi:type="number">0</item> - </field> - <field name="payment/authorizenet_directpost/cgi_url" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">https://test.authorize.net/gateway/transact.dll</item> - </field> - <field name="payment/authorizenet_directpost/cgi_url_td" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">https://apitest.authorize.net/xml/v1/request.api</item> - </field> - <field name="payment/authorizenet_directpost/debug" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - <field name="payment/authorizenet_directpost/useccv" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - </dataset> - <dataset name="authorizenet_fraud_review_rollback"> - <field name="payment/authorizenet_directpost/active" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string">No</item> - <item name="value" xsi:type="number">0</item> - </field> - </dataset> - <dataset name="authorizenet_authorize_capture"> - <field name="payment/authorizenet_directpost/payment_action" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">authorize_capture</item> - </field> - </dataset> - <dataset name="authorizenet_authorize_capture_rollback"> - <field name="payment/authorizenet_directpost/payment_action" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string">authorize</item> - </field> - </dataset> - <dataset name="authorizenet_wrong_credentials"> - <field name="payment/authorizenet_directpost/trans_md5" xsi:type="array"> - <item name="scope" xsi:type="string">payment</item> - <item name="scope_id" xsi:type="number">1</item> - <item name="label" xsi:type="string"/> - <item name="value" xsi:type="string"></item> - </field> - </dataset> - </repository> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/TransactionSearch.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/TransactionSearch.xml deleted file mode 100644 index 19b2d5eededa6..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/Repository/TransactionSearch.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?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="../../../../../../vendor/magento/mtf/Magento/Mtf/Repository/etc/repository.xsd"> - <repository class="Magento\Authorizenet\Test\Repository\TransactionSearch"> - <dataset name="unsettled"> - <field name="settlement_date_from" xsi:type="string">Unsettled</field> - <field name="settlement_date_to" xsi:type="string">Unsettled</field> - </dataset> - </repository> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/AuthorizenetFraudCheckoutTest.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/AuthorizenetFraudCheckoutTest.php deleted file mode 100644 index ca0c473eb685f..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/AuthorizenetFraudCheckoutTest.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\TestCase; - -use Magento\Mtf\TestCase\Scenario; - -/** - * Preconditions: - * 1. Configure payment method. - * 2. Create products. - * - * Steps: - * 1. Log in Storefront. - * 2. Add products to the Shopping Cart. - * 3. Click the 'Proceed to Checkout' button. - * 4. Fill shipping information. - * 5. Select shipping method. - * 6. Select payment method. - * 7. Click 'Place Order' button. - * 8. Log in to Authorize.Net sandbox. - * 9. Accept transaction. - * 10. Log in to Magento Admin. - * 11. Open order. - * 12. Perform assertions. - * - * @group Checkout - * @ZephyrId MAGETWO-38379 - */ -class AuthorizenetFraudCheckoutTest extends Scenario -{ - /* tags */ - const TEST_TYPE = '3rd_party_test'; - /* end tags */ - - /** - * Runs one page checkout test. - * - * @return void - */ - public function test() - { - $this->executeScenario(); - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/AuthorizenetFraudCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/AuthorizenetFraudCheckoutTest.xml deleted file mode 100644 index 9ba32a4cc7a6b..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/AuthorizenetFraudCheckoutTest.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Authorizenet\Test\TestCase\AuthorizenetFraudCheckoutTest" summary="Accept transaction for order placed via Authorize.Net."> - <variation name="AuthorizenetFraudCheckoutAccept" summary="Accept transaction with 'Authorize and hold for review' fraud action for order placed with 'Authorize and Capture' payment action" ticketId="MAGETWO-38379"> - <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> - <data name="customer/dataset" xsi:type="string">default</data> - <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> - <data name="checkoutMethod" xsi:type="string">login</data> - <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> - <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="prices" xsi:type="array"> - <item name="grandTotal" xsi:type="string">15.00</item> - </data> - <data name="orderBeforeAccept" xsi:type="array"> - <item name="invoiceStatus" xsi:type="string">Pending</item> - <item name="orderStatus" xsi:type="string">Suspected Fraud</item> - <item name="buttonsAvailable" xsi:type="string">Back, Send Email, Get Payment Update</item> - </data> - <data name="payment/method" xsi:type="string">authorizenet_directpost</data> - <data name="creditCard/dataset" xsi:type="string">visa_default</data> - <data name="sandboxCustomer/dataset" xsi:type="string">sandbox_fraud_hold_review</data> - <data name="transactionSearch/dataset" xsi:type="string">unsettled</data> - <data name="configData" xsi:type="string">authorizenet_fraud_review, authorizenet_authorize_capture</data> - <data name="status" xsi:type="string">Processing</data> - <data name="invoiceStatus" xsi:type="string">Paid</data> - <data name="orderButtonsAvailable" xsi:type="string">Back, Send Email, Credit Memo, Hold, Ship, Reorder</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S1</data> - <constraint name="Magento\Sales\Test\Constraint\AssertInvoiceStatusInOrdersGrid" /> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutDeclinedTest.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutDeclinedTest.xml deleted file mode 100644 index 00c1a1557e05b..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutDeclinedTest.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Checkout\Test\TestCase\OnePageCheckoutDeclinedTest" summary="Error message during OnePageCheckout"> - <variation name="OnePageCheckoutAuthorizenetWrongCredentials" summary="Error during place order flow with Authorize.net" ticketId="MAGETWO-69995"> - <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> - <data name="customer/dataset" xsi:type="string">default</data> - <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> - <data name="checkoutMethod" xsi:type="string">login</data> - <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> - <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="payment/method" xsi:type="string">authorizenet_directpost</data> - <data name="creditCard/dataset" xsi:type="string">visa_default</data> - <data name="configData" xsi:type="string">authorizenet, authorizenet_wrong_credentials</data> - <data name="expectedErrorMessage" xsi:type="string">A server error stopped your order from being placed. Please try to place your order again.</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S2</data> - <constraint name="Magento\Checkout\Test\Constraint\AssertCheckoutErrorMessage" /> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutPaymentMethodDataPersistenceWithDiscountTest.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutPaymentMethodDataPersistenceWithDiscountTest.xml deleted file mode 100644 index b979745a99b96..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutPaymentMethodDataPersistenceWithDiscountTest.xml +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\SalesRule\Test\TestCase\OnePageCheckoutPaymentMethodDataPersistenceWithDiscountTest" summary="Checkout with Authorize.net credit card on Storefront with discount applied during checkout"> - <variation name="OnePageCheckoutPaymentMethodDataPersistWithDiscountTest1" summary="Checkout with Authorize.net credit card on Storefront with discount applied during checkout" ticketId="MAGETWO-69657"> - <data name="description" xsi:type="string">Use saved for Authorize.net credit card on checkout</data> - <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> - <data name="customer/dataset" xsi:type="string">default</data> - <data name="salesRule" xsi:type="string">active_sales_rule_for_all_groups</data> - <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> - <data name="checkoutMethod" xsi:type="string">login</data> - <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> - <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="payment/method" xsi:type="string">authorizenet_directpost</data> - <data name="paymentForm" xsi:type="string">default</data> - <data name="creditCard/dataset" xsi:type="string">visa_default</data> - <data name="creditCard/data/payment_code" xsi:type="string">authorizenet</data> - <data name="prices" xsi:type="array"> - <item name="grandTotal" xsi:type="string">10.00</item> - </data> - <data name="creditCardSave" xsi:type="string">Yes</data> - <data name="configData" xsi:type="string">authorizenet</data> - <data name="status" xsi:type="string">Processing</data> - <data name="tag" xsi:type="string">severity:S1</data> - <constraint name="Magento\Authorizenet\Test\Constraint\AssertCreditCardNumberOnOnePageCheckout" /> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutTest.xml deleted file mode 100644 index a7eaaada1be83..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/OnePageCheckoutTest.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Checkout\Test\TestCase\OnePageCheckoutTest" summary="One page check out with Authorize.Net Direct Post payment method."> - <variation name="OnePageCheckoutAuthorizenetTestVariation1" summary="CheckOut with Authorize.Net Direct Post" ticketId="MAGETWO-59170"> - <data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data> - <data name="customer/dataset" xsi:type="string">default</data> - <data name="checkoutMethod" xsi:type="string">login</data> - <data name="shippingAddress/dataset" xsi:type="string">US_address_1_without_email</data> - <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> - <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="prices" xsi:type="array"> - <item name="grandTotal" xsi:type="string">15.00</item> - </data> - <data name="payment/method" xsi:type="string">authorizenet_directpost</data> - <data name="creditCard/dataset" xsi:type="string">visa_default</data> - <data name="configData" xsi:type="string">authorizenet</data> - <data name="status" xsi:type="string">Processing</data> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> - <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> - <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> - <constraint name="Magento\Sales\Test\Constraint\AssertAuthorizationInCommentsHistory" /> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/ReorderOrderEntityTest.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/ReorderOrderEntityTest.xml deleted file mode 100644 index b0f4856e28d0d..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestCase/ReorderOrderEntityTest.xml +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Sales\Test\TestCase\ReorderOrderEntityTest" summary="Reorder Order from Admin using Authorize.Net Direct Post payment method." ticketId="MAGETWO-69939"> - <variation name="ReorderOrderEntityAuthorizenetTestVariation1"> - <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S2</data> - <data name="description" xsi:type="string">Reorder placed order using Authorize.Net payment method (update products, billing address).</data> - <data name="order/dataset" xsi:type="string">default</data> - <data name="customer/dataset" xsi:type="string">default</data> - <data name="billingAddress/dataset" xsi:type="string">US_address_1_without_email</data> - <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> - <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="prices" xsi:type="array"> - <item name="grandTotal" xsi:type="string">565.00</item> - </data> - <data name="payment/method" xsi:type="string">authorizenet_directpost</data> - <data name="configData" xsi:type="string">authorizenet</data> - <data name="creditCard/dataset" xsi:type="string">visa_default_admin</data> - <data name="previousOrderStatus" xsi:type="string">Processing</data> - <data name="status" xsi:type="string">Processing</data> - <data name="orderButtonsAvailable" xsi:type="string">Back, Reorder, Cancel, Send Email, Hold, Invoice, Ship, Edit</data> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderSuccessCreateMessage" /> - <constraint name="Magento\Sales\Test\Constraint\AssertReorderStatusIsCorrect" /> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> - <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestStep/AcceptTransactionOnAuthorizenetStep.php b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestStep/AcceptTransactionOnAuthorizenetStep.php deleted file mode 100644 index 0ff6bb486cf81..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/TestStep/AcceptTransactionOnAuthorizenetStep.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Authorizenet\Test\TestStep; - -use Magento\Authorizenet\Test\Fixture\AuthorizenetSandboxCustomer; -use Magento\Authorizenet\Test\Fixture\TransactionSearch; -use Magento\Authorizenet\Test\Page\Sandbox\Main; -use Magento\Mtf\Client\BrowserInterface; -use Magento\Mtf\TestStep\TestStepInterface; -use Magento\Sales\Test\Constraint\AssertInvoiceStatusInOrdersGrid; -use Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable; -use Magento\Sales\Test\Page\Adminhtml\OrderIndex; -use Magento\Sales\Test\Page\Adminhtml\SalesOrderView; - -/** - * Accept transaction on Authorize.Net sandbox. - */ -class AcceptTransactionOnAuthorizenetStep implements TestStepInterface -{ - /** - * Authorize.Net Sandbox customer fixture. - * - * @var AuthorizenetSandboxCustomer - */ - private $sandboxCustomer; - - /** - * Authorize.Net Sandbox account main page. - * - * @var Main - */ - private $main; - - /** - * Sales Order View page. - * - * @var SalesOrderView - */ - private $salesOrderView; - - /** - * Order Index page. - * - * @var OrderIndex - */ - private $salesOrder; - - /** - * Order id. - * - * @var string - */ - private $orderId; - - /** - * Assert invoice status on order page in Admin. - * - * @var AssertInvoiceStatusInOrdersGrid - */ - private $assertInvoiceStatusInOrdersGrid; - - /** - * Unsettled order data. - * - * @var array - */ - private $orderBeforeAccept; - - /** - * Assert that specified in data set buttons exist on order page in Admin. - * - * @var AssertOrderButtonsAvailable - */ - private $assertOrderButtonsAvailable; - - /** - * Client Browser instance. - * - * @var BrowserInterface - */ - private $browser; - - /** - * Form frame selector. - * - * @var string - */ - private $frame = 'frameset > frame'; - - /** - * Transaction search fixture. - * - * @var TransactionSearch - */ - private $transactionSearch; - - /** - * @param AuthorizenetSandboxCustomer $sandboxCustomer - * @param TransactionSearch $transactionSearch - * @param Main $main - * @param SalesOrderView $salesOrderView - * @param OrderIndex $salesOrder - * @param AssertInvoiceStatusInOrdersGrid $assertInvoiceStatusInOrdersGrid - * @param AssertOrderButtonsAvailable $assertOrderButtonsAvailable - * @param BrowserInterface $browser - * @param array $orderBeforeAccept - * @param string $orderId - * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) - */ - public function __construct( - AuthorizenetSandboxCustomer $sandboxCustomer, - TransactionSearch $transactionSearch, - Main $main, - SalesOrderView $salesOrderView, - OrderIndex $salesOrder, - AssertInvoiceStatusInOrdersGrid $assertInvoiceStatusInOrdersGrid, - AssertOrderButtonsAvailable $assertOrderButtonsAvailable, - BrowserInterface $browser, - array $orderBeforeAccept, - $orderId - ) { - $this->sandboxCustomer = $sandboxCustomer; - $this->transactionSearch = $transactionSearch; - $this->main = $main; - $this->salesOrderView = $salesOrderView; - $this->salesOrder = $salesOrder; - $this->assertInvoiceStatusInOrdersGrid = $assertInvoiceStatusInOrdersGrid; - $this->assertOrderButtonsAvailable = $assertOrderButtonsAvailable; - $this->browser = $browser; - $this->orderBeforeAccept = $orderBeforeAccept; - $this->orderId = $orderId; - } - - /** - * Accept transaction on sandbox.authorize.net account. - * - * @return void - * @throws \Exception - */ - public function run() - { - $this->assertInvoiceStatusInOrdersGrid->processAssert( - $this->salesOrderView, - $this->orderBeforeAccept['invoiceStatus'], - $this->orderId - ); - $this->assertOrderButtonsAvailable->processAssert( - $this->salesOrderView, - $this->orderBeforeAccept['buttonsAvailable'] - ); - $this->salesOrder->open(); - $this->salesOrder->getSalesOrderGrid()->searchAndOpen(['id' => $this->orderId]); - - /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */ - $infoTab = $this->salesOrderView->getOrderForm()->openTab('info')->getTab('info'); - $latestComment = $infoTab->getCommentsHistoryBlock()->getLatestComment(); - if (!preg_match('/"(\d+)"/', $latestComment['comment'], $matches)) { - throw new \Exception('Comment with transaction id cannot be found.'); - } - $transactionId = $matches[1]; - $this->main->open(); - $this->browser->switchToFrame($this->browser->find($this->frame)->getLocator()); - $this->main->getLoginForm()->fill($this->sandboxCustomer)->login(); - $this->main->getModalBlock()->acceptNotification(); - $this->main->getMenuBlock()->openSearchMenu(); - $this->main->getSearchForm()->fill($this->transactionSearch)->search(); - $this->main->getTransactionsGridBlock()->openTransaction($transactionId)->approveTransaction(); - } -} diff --git a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/etc/testcase.xml b/dev/tests/functional/tests/app/Magento/Authorizenet/Test/etc/testcase.xml deleted file mode 100644 index 5ce1f811b4fbb..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Authorizenet/Test/etc/testcase.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?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="../../../../../../vendor/magento/mtf/Magento/Mtf/TestCase/etc/testcase.xsd"> - <scenario name="AuthorizenetFraudCheckoutTest" firstStep="setupConfiguration"> - <step name="setupConfiguration" module="Magento_Config" next="createProducts" /> - <step name="createProducts" module="Magento_Catalog" next="addProductsToTheCart" /> - <step name="addProductsToTheCart" module="Magento_Checkout" next="proceedToCheckout" /> - <step name="proceedToCheckout" module="Magento_Checkout" next="createCustomer" /> - <step name="createCustomer" module="Magento_Customer" next="selectCheckoutMethod" /> - <step name="selectCheckoutMethod" module="Magento_Checkout" next="fillShippingAddress" /> - <step name="fillShippingAddress" module="Magento_Checkout" next="fillShippingMethod" /> - <step name="fillShippingMethod" module="Magento_Checkout" next="selectPaymentMethod" /> - <step name="selectPaymentMethod" module="Magento_Checkout" next="fillBillingInformation" /> - <step name="fillBillingInformation" module="Magento_Checkout" next="placeOrder" /> - <step name="placeOrder" module="Magento_Checkout" next="acceptTransactionOnAuthorizenet" /> - <step name="acceptTransactionOnAuthorizenet" module="Magento_Authorizenet" next="getPaymentUpdate" /> - <step name="getPaymentUpdate" module="Magento_Sales" /> - </scenario> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/GlobalSearch/Query.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/GlobalSearch/Query.php index a386584a2817b..5e5034b8ec099 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/GlobalSearch/Query.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Fixture/GlobalSearch/Query.php @@ -32,7 +32,7 @@ public function __construct(FixtureFactory $fixtureFactory, $data, array $params { $this->params = $params; $explodedData = explode('::', $data); - switch (sizeof($explodedData)) { + switch (count($explodedData)) { case 1: $this->data = $explodedData[0]; break; diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php index 01d8401b22fe1..39f5866a3c2ad 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php @@ -47,6 +47,7 @@ public function persist(FixtureInterface $fixture = null) $loginForm->fill($fixture); $loginForm->submit(); $loginPage->waitForHeaderBlock(); + $loginPage->dismissAdminUsageNotification(); } } } diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/AdminAuthLogin.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/AdminAuthLogin.php index c836c4db81ef1..4b72bb836523a 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/AdminAuthLogin.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/AdminAuthLogin.php @@ -40,6 +40,11 @@ class AdminAuthLogin extends Page */ protected $messagesBlock = '.messages'; + /** + * Admin Analytics selector + */ + protected $adminUsageSelector ='.modal-inner-wrap'; + /** * Constructor. */ @@ -75,13 +80,24 @@ public function getHeaderBlock() /** * Get global messages block. * - * @return \Magento\Backend\Test\Block\Messages + * @return \Magento\Ui\Test\Block\Adminhtml\Modal + */ public function getMessagesBlock() { return Factory::getBlockFactory()->getMagentoBackendMessages($this->browser->find($this->messagesBlock)); } + /** + * Get modal block + * + * @return void + */ + public function getModalBlock() + { + return Factory::getBlockFactory()->getMagentoUiAdminhtmlModal($this->browser->find($this->adminUsageSelector)); + } + /** * Wait for Header block is visible in the page. * @@ -98,4 +114,14 @@ function () use ($browser, $selector) { } ); } + + /** + * Dismiss admin usage notification + * + * @return void + */ + public function dismissAdminUsageNotification() + { + $this->getModalBlock()->dismissIfModalAppears(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml index 2d7e609c1c389..0694966c7eaa5 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml @@ -9,6 +9,7 @@ <testCase name="Magento\Backend\Test\TestCase\ConfigPageVisibilityTest" summary="Check Developer section and Locale field"> <variation name="VisibilityOfDeveloperSectionAndLocaleField" summary="Check Developer section and Locale field" ticketId="MAGETWO-63625, MAGETWO-63624"> <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Backend\Test\Constraint\AssertLocaleCodeVisibility" /> <constraint name="Magento\Backend\Test\Constraint\AssertDeveloperSectionVisibility" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ExpireSessionTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ExpireSessionTest.xml index e67ceb99f2eef..195d1330ae78a 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ExpireSessionTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ExpireSessionTest.xml @@ -8,12 +8,14 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\ExpireSessionTest" summary="Admin Session Expire" ticketId="MAGETWO-47723"> <variation name="ExpireSessionTestVariation1" summary="Check that session expires according with time settings applied in configuration" ticketId="MAGETWO-47722"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">default_cookie_lifetime_60_seconds</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="sessionLifetimeInSeconds" xsi:type="number">60</data> <constraint name="Magento\Cms\Test\Constraint\AssertAuthorizationLinkIsVisibleOnStoreFront" /> </variation> <variation name="ExpireAdminSession" summary="Expire Admin Session" ticketId="MAGETWO-47723"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">admin_session_lifetime_60_seconds</data> <data name="sessionLifetimeInSeconds" xsi:type="number">60</data> <constraint name="Magento\Backend\Test\Constraint\AssertAdminLoginPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php index f1ab255013280..a06ee2332704a 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php @@ -311,7 +311,7 @@ public function fillBundleOptions($bundleOptions) { foreach ($bundleOptions as $option) { $selector = sprintf($this->bundleOptionBlock, $option['title']); - $useDefault = isset($option['use_default']) && strtolower($option['use_default']) == 'true' ? true : false; + $useDefault = isset($option['use_default']) && strtolower($option['use_default']) == 'true'; if (!$useDefault) { /** @var Option $optionBlock */ $optionBlock = $this->blockFactory->create( diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml index 525e6b47374a0..028dfc6d109ea 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml @@ -31,7 +31,7 @@ </category_ids> <quantity_and_stock_status composite="1"> <qty> - <selector>fieldset[data-index="container_quantity_and_stock_status_qty"] [name="product[quantity_and_stock_status][qty]"]</selector> + <selector>fieldset[data-index="quantity_and_stock_status_qty"] [name="product[quantity_and_stock_status][qty]"]</selector> </qty> <is_in_stock> <selector>[data-index="quantity_and_stock_status"] [name="product[quantity_and_stock_status][is_in_stock]"]</selector> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml index fafd9842cc749..c5036555b6635 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Category.xml @@ -49,7 +49,6 @@ <field name="use_parent_category_settings" is_required="" group="design" /> <field name="theme" is_required="" group="design" /> <field name="layout" is_required="" group="design" /> - <field name="layout_update_xml" is_required="" group="design" /> <field name="apply_design_to_products" is_required="" group="design" /> <field name="schedule_update_from" is_required="" group="schedule_design_update" /> <field name="schedule_update_to" is_required="" group="schedule_design_update" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php index 23e0236fe7baa..70d2730868dbf 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php @@ -52,7 +52,7 @@ class TaxClass extends DataSource public function __construct(FixtureFactory $fixtureFactory, array $params, $data = []) { $this->params = $params; - if ((!isset($data['dataset']) && !isset($data['tax_product_class']))) { + if (!isset($data['dataset']) && !isset($data['tax_product_class'])) { $this->data = $data; return; } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/Category/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/Category/Curl.php index 5c54b366b7ab4..fb9d6e9772c74 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/Category/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/Category/Curl.php @@ -251,8 +251,13 @@ protected function getBlockId($landingName) $curl->write($url, [], CurlInterface::GET); $response = $curl->read(); $curl->close(); - preg_match('~\{"value":"(\d+)","label":"' . preg_quote($landingName) . '"\}~', $response, $matches); - $id = isset($matches[1]) ? (int)$matches[1] : null; + $id = null; + //Finding block option in 'Add block' options UI data. + preg_match('~\{[^\{\}]*?"label":"' . preg_quote($landingName) . '"[^\{\}]*?\}~', $response, $matches); + if (!empty($matches)) { + $blockOption = json_decode($matches[0], true); + $id = (int)$blockOption['value']; + } return $id; } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml index 69093b8adb8db..5ea1b692e3eb9 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml @@ -37,7 +37,6 @@ <data name="category/data/meta_keywords" xsi:type="string">custom meta keywords %isolation%</data> <data name="category/data/meta_description" xsi:type="string">Custom meta description %isolation%</data> <data name="category/data/layout" xsi:type="string">2 columns with right bar</data> - <data name="category/data/layout_update_xml" xsi:type="string"><referenceContainer name="catalog.leftnav" remove="true"/></data> <data name="category/data/new_theme" xsi:type="string">Magento Luma</data> <data name="category/data/apply_design_to_products" xsi:type="string">Yes</data> <data name="category/data/schedule_update_from" xsi:type="string">01/10/2014</data> @@ -80,7 +79,6 @@ <data name="category/data/meta_description" xsi:type="string">Custom meta description %isolation%</data> <data name="category/data/category_products/dataset" xsi:type="string">catalogProductSimple::default,catalogProductSimple::default</data> <data name="category/data/layout" xsi:type="string">2 columns with right bar</data> - <data name="category/data/layout_update_xml" xsi:type="string"><referenceContainer name="content.aside" remove="true"/></data> <data name="category/data/new_theme" xsi:type="string">Magento Luma</data> <data name="category/data/apply_design_to_products" xsi:type="string">Yes</data> <data name="category/data/schedule_update_from" xsi:type="string">01/10/2014</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php index 90cd6bdb76328..b97accbf87a93 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php @@ -12,6 +12,7 @@ use Magento\Downloadable\Test\Block\Adminhtml\Catalog\Product\Edit\Section\Downloadable; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Test Creation for ProductTypeSwitchingOnUpdating @@ -60,22 +61,32 @@ class ProductTypeSwitchingOnUpdateTest extends Injectable */ protected $fixtureFactory; + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Injection data. * * @param CatalogProductIndex $catalogProductIndex * @param CatalogProductEdit $catalogProductEdit * @param FixtureFactory $fixtureFactory + * @param EnvWhitelist $envWhitelist * @return void */ public function __inject( CatalogProductIndex $catalogProductIndex, CatalogProductEdit $catalogProductEdit, - FixtureFactory $fixtureFactory + FixtureFactory $fixtureFactory, + EnvWhitelist $envWhitelist ) { $this->catalogProductIndex = $catalogProductIndex; $this->catalogProductEdit = $catalogProductEdit; $this->fixtureFactory = $fixtureFactory; + $this->envWhitelist = $envWhitelist; } /** @@ -89,6 +100,7 @@ public function __inject( public function test($productOrigin, $product, $actionName) { // Preconditions + $this->envWhitelist->addHost('example.com'); list($fixtureClass, $dataset) = explode('::', $productOrigin); $productOrigin = $this->fixtureFactory->createByCode(trim($fixtureClass), ['dataset' => trim($dataset)]); $productOrigin->persist(); @@ -144,5 +156,6 @@ protected function clearDownloadableData() $downloadableInfoTab = $this->catalogProductEdit->getProductForm()->getSection('downloadable_information'); $downloadableInfoTab->getDownloadableBlock('Links')->clearDownloadableData(); $downloadableInfoTab->setIsDownloadable('No'); + $this->envWhitelist->removeHost('example.com'); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml index 5fa1cfe5e5911..732dac98e0779 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml @@ -11,6 +11,7 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">configurableProduct::default</data> <data name="actionName" xsi:type="string">-</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -34,6 +35,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::required_fields</data> <data name="actionName" xsi:type="string">deleteVariations</data> @@ -48,6 +50,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">-</data> @@ -60,6 +63,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> @@ -71,6 +75,7 @@ <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation8"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> @@ -78,6 +83,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation9"> + <data name="tag" xsi:type="string">test_type:acceptance_test</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">clearDownloadableData</data> @@ -97,6 +103,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml index 0ff402daca07d..4f74b7ff554db 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml @@ -12,7 +12,7 @@ <strategy>css selector</strategy> <fields> <is_active> - <input>select</input> + <input>switcher</input> </is_active> <website_ids> <selector>[name='website_ids']</selector> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php index 8ac13d407e433..695323990063a 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php @@ -19,7 +19,7 @@ class AssertCatalogPriceRuleInGrid extends AbstractConstraint * Fields used to filter rows in the grid. * @var array */ - protected $fieldsToFilter = ['name', 'is_active']; + protected $fieldsToFilter = ['name']; /** * Assert that data in grid on Catalog Price Rules page according to fixture diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml index 49bf36b0325ba..1f16e28c067d8 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml @@ -10,7 +10,7 @@ <variation name="CatalogRule_Create_Active_AdminOnly"> <data name="catalogPriceRule/data/name" xsi:type="string">CatalogPriceRule %isolation%</data> <data name="catalogPriceRule/data/description" xsi:type="string">Catalog Price Rule Description</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">Wholesale</data> <data name="catalogPriceRule/data/simple_action" xsi:type="string">Apply as percentage of original</data> @@ -24,7 +24,7 @@ <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogPriceRule/data/name" xsi:type="string">CatalogPriceRule %isolation%</data> <data name="catalogPriceRule/data/description" xsi:type="string">Catalog Price Rule Description</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Inactive</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">No</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">General</data> <data name="catalogPriceRule/data/condition" xsi:type="string">-</data> @@ -39,7 +39,7 @@ <variation name="CatalogRule_Create_ForGuestUsers_AdjustPriceToPercentage"> <data name="product" xsi:type="string">MAGETWO-23036</data> <data name="catalogPriceRule/data/name" xsi:type="string">rule_name%isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">NOT LOGGED IN</data> <data name="conditionEntity" xsi:type="string">category</data> @@ -54,7 +54,7 @@ <data name="customer/dataset" xsi:type="string">customer_with_new_customer_group</data> <data name="product" xsi:type="string">simple_10_dollar</data> <data name="catalogPriceRule/data/name" xsi:type="string">rule_name%isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="conditionEntity" xsi:type="string">category</data> <data name="catalogPriceRule/data/conditions" xsi:type="string">[Category|is|%category_id%]</data> @@ -68,7 +68,7 @@ <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> <data name="product" xsi:type="string">product_with_custom_color_attribute</data> <data name="catalogPriceRule/data/name" xsi:type="string">Catalog Price Rule %isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">NOT LOGGED IN</data> <data name="conditionEntity" xsi:type="string">attribute</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml index 6c8e86b24ae60..e2916432c8eb7 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml @@ -10,7 +10,7 @@ <variation name="CatalogRule_Update_Name_Status"> <data name="catalogPriceRuleOriginal/dataset" xsi:type="string">active_catalog_price_rule_with_conditions</data> <data name="catalogPriceRule/data/name" xsi:type="string">New Catalog Price Rule Name %isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Inactive</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">No</data> <data name="saveAction" xsi:type="string">save</data> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleSuccessSaveMessage" /> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleNoticeMessage" /> @@ -24,7 +24,7 @@ <data name="catalogPriceRuleOriginal/dataset" xsi:type="string">active_catalog_price_rule_with_conditions</data> <data name="catalogPriceRule/data/name" xsi:type="string">New Catalog Price Rule Name %isolation%</data> <data name="catalogPriceRule/data/description" xsi:type="string">New Catalog Price Rule Description %isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/conditions" xsi:type="string">[Category|is|%category_1%]</data> <data name="catalogPriceRule/data/simple_action" xsi:type="string">Apply as fixed amount</data> <data name="catalogPriceRule/data/discount_amount" xsi:type="string">35</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml index 4744fa7756c4e..9a26386c82cb8 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\AdvancedSearchEntityTest" summary="Use Advanced Search" ticketId="MAGETWO-24729"> <variation name="AdvancedSearchEntityTestVariation1" summary="Use Advanced Search to Find the Product" ticketId="MAGETWO-12421"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> <data name="productSearch/data/name" xsi:type="string">abc_dfj</data> @@ -16,6 +16,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by name</data> <data name="products/simple_1" xsi:type="string">-</data> <data name="products/simple_2" xsi:type="string">Yes</data> @@ -23,6 +24,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial name</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -30,6 +32,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by sku</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -37,6 +40,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial sku</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -44,6 +48,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial sku and description</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -52,6 +57,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by description</data> <data name="products/simple_1" xsi:type="string">-</data> <data name="products/simple_2" xsi:type="string">Yes</data> @@ -59,6 +65,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation8"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by short description</data> <data name="products/simple_1" xsi:type="string">-</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -66,6 +73,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial short description</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -73,6 +81,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">Yes</data> @@ -80,6 +89,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by price from and price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -88,6 +98,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation12"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by name, sku, description, short description, price from and price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -100,6 +111,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation13"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by name, sku, description, short description, price from and price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -112,11 +124,13 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation14"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Negative product search</data> <data name="productSearch/data/name" xsi:type="string">Negative_product_search</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchNoResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation15" summary="Do Advanced Search without entering data" ticketId="MAGETWO-14859"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="issue" xsi:type="string">MAGETWO-18537: "Enter a search term and try again." error message is missed in Advanced Search</data> <data name="productSearch/data/name" xsi:type="string" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchEmptyTerm" /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar/Item.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar/Item.php index c00687d91c1ee..038c411768969 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar/Item.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Sidebar/Item.php @@ -62,6 +62,7 @@ class Item extends Sidebar */ public function removeItemFromMiniCart() { + $this->waitForDeleteButtonVisible(); $this->_rootElement->find($this->removeItem)->click(); $element = $this->browser->find($this->confirmModal); /** @var \Magento\Ui\Test\Block\Adminhtml\Modal $modal */ @@ -70,6 +71,23 @@ public function removeItemFromMiniCart() $modal->waitModalWindowToDisappear(); } + /** + * Wait for Delete button is visible in the block. + * + * @return bool|null + */ + private function waitForDeleteButtonVisible() + { + $rootElement = $this->_rootElement; + $deleteButtonSelector = $this->removeItem; + return $rootElement->waitUntil( + function () use ($rootElement, $deleteButtonSelector) { + $element = $rootElement->find($deleteButtonSelector); + return $element->isVisible() ? true : null; + } + ); + } + /** * Click "Edit item" button. * diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php index 7d6bd93180230..fba5a2b062343 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.php @@ -14,6 +14,7 @@ use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; use Magento\Mtf\Util\Command\Cli\Cache; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Preconditions: @@ -99,6 +100,13 @@ class AddProductsToShoppingCartEntityTest extends Injectable */ private $cache; + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Prepare test data. * @@ -108,6 +116,7 @@ class AddProductsToShoppingCartEntityTest extends Injectable * @param CheckoutCart $cartPage * @param TestStepFactory $testStepFactory * @param Cache $cache + * @param EnvWhitelist $envWhitelist * @return void */ public function __prepare( @@ -116,7 +125,8 @@ public function __prepare( CatalogProductView $catalogProductView, CheckoutCart $cartPage, TestStepFactory $testStepFactory, - Cache $cache + Cache $cache, + EnvWhitelist $envWhitelist ) { $this->browser = $browser; $this->fixtureFactory = $fixtureFactory; @@ -124,6 +134,7 @@ public function __prepare( $this->cartPage = $cartPage; $this->testStepFactory = $testStepFactory; $this->cache = $cache; + $this->envWhitelist = $envWhitelist; } /** @@ -146,6 +157,7 @@ public function test( // Preconditions $this->configData = $configData; $this->flushCache = $flushCache; + $this->envWhitelist->addHost('example.com'); $this->testStepFactory->create( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, @@ -224,7 +236,7 @@ public function tearDown() $_ENV['app_frontend_url'] = preg_replace('/(http[s]?)/', 'http', $_ENV['app_frontend_url']); $this->cache->flush(); } - + $this->envWhitelist->removeHost('example.com'); $this->testStepFactory->create( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $this->configData, 'rollback' => true, 'flushCache' => $this->flushCache] diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.php index 54f59b03ef81d..6f5512b2e8293 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\TestCase; use Magento\Mtf\TestCase\Scenario; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Preconditions: @@ -43,6 +44,23 @@ class OnePageCheckoutOfflinePaymentMethodsTest extends Scenario const SEVERITY = 'S0'; /* end tags */ + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + + /** + * Perform needed injections + * + * @param EnvWhitelist $envWhitelist + */ + public function __inject(EnvWhitelist $envWhitelist) + { + $this->envWhitelist = $envWhitelist; + } + /** * Runs one page checkout test. * @@ -50,6 +68,17 @@ class OnePageCheckoutOfflinePaymentMethodsTest extends Scenario */ public function test() { + $this->envWhitelist->addHost('example.com'); $this->executeScenario(); } + + /** + * Clean data after running test. + * + * @return void + */ + public function tearDown() + { + $this->envWhitelist->removeHost('example.com'); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPagerTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPagerTest.xml index 8b6b69b73b894..e03351de7a2b1 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPagerTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPagerTest.xml @@ -8,24 +8,24 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\ShoppingCartPagerTest" summary="Verify pager on Shopping Cart" ticketId="MAGETWO-63337"> <variation name="ShoppingCartPagerTestFor20ItemsPerPageAnd20Products" summary="Verify pager is NOT presented on Shopping Cart page if qty of products = 20, by default system configuration" ticketId="MAGETWO-63337"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertPagersNotPresentInShoppingCart"/> </variation> <variation name="ShoppingCartPagerTestFor20ItemsPerPageAnd21Products" summary="Verify pager is presented on Shopping Cart page if items qty=21, by default system configuration" ticketId="MAGETWO-63338"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="config/dataset" xsi:type="string">default_number_of_items_per_page_on_shopping_cart</data> <data name="products/21" xsi:type="string">catalogProductSimple::default</data> <constraint name="Magento\Checkout\Test\Constraint\AssertPagersPresentInShoppingCart"/> <constraint name="Magento\Checkout\Test\Constraint\AssertPagersSummaryText"/> </variation> <variation name="ShoppingCartPagerTestFor20ItemsPerPageAndRemovingOneProduct" summary="Verify pager is disapeared from Shopping Cart page if change qty from 21 to 20, by default system configuration" ticketId="MAGETWO-63339"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="products/21" xsi:type="string">catalogProductSimple::default</data> <data name="itemsToRemove" xsi:type="string">1</data> <constraint name="Magento\Checkout\Test\Constraint\AssertPagersNotPresentInShoppingCart"/> </variation> <variation name="ShoppingCartPagerTestForOneItemPerPageAnd20Products" summary="Verify Pager is presented on Shopping Cart page with non-default system configuration" ticketId="MAGETWO-63340"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="configData" xsi:type="string">one_item_per_page_on_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertPagersPresentInShoppingCart" /> <constraint name="Magento\Checkout\Test\Constraint\AssertPagersSummaryText" /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php index af267cfa30ec1..36b4f4b3eb39b 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php @@ -12,6 +12,7 @@ use Magento\Mtf\Fixture\FixtureInterface; use Magento\Mtf\TestCase\Injectable; use Magento\Customer\Test\Fixture\Customer; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Preconditions: @@ -58,22 +59,32 @@ class UpdateProductFromMiniShoppingCartEntityTest extends Injectable */ protected $fixtureFactory; + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Inject data. * * @param CmsIndex $cmsIndex * @param CatalogProductView $catalogProductView * @param FixtureFactory $fixtureFactory + * @param EnvWhitelist $envWhitelist * @return void */ public function __inject( CmsIndex $cmsIndex, CatalogProductView $catalogProductView, - FixtureFactory $fixtureFactory + FixtureFactory $fixtureFactory, + EnvWhitelist $envWhitelist ) { $this->cmsIndex = $cmsIndex; $this->catalogProductView = $catalogProductView; $this->fixtureFactory = $fixtureFactory; + $this->envWhitelist = $envWhitelist; } /** @@ -97,6 +108,7 @@ public function test( Customer $customer = null ) { // Preconditions: + $this->envWhitelist->addHost('example.com'); if ($customer !== null) { $customer->persist(); } @@ -162,4 +174,14 @@ protected function addToCart(FixtureInterface $product) ); $addToCartStep->run(); } + + /** + * Clean data after running test. + * + * @return void + */ + public function tearDown() + { + $this->envWhitelist->removeHost('example.com'); + } } diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml index 95d99f9fa76cd..f98f9ca7cfe24 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml @@ -18,7 +18,7 @@ <input>select</input> </mode> <stores> - <selector>[name="stores[0]"]</selector> + <selector>[name="stores[]"]</selector> <input>multiselectgrouplist</input> </stores> <checkbox_text /> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/UpdateCmsPageRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/UpdateCmsPageRewriteEntityTest.xml index 6ff68599beeb3..1125ce8d916c1 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/UpdateCmsPageRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/UpdateCmsPageRewriteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Cms\Test\TestCase\UpdateCmsPageRewriteEntityTest" summary="Update Cms Page URL Rewrites " ticketId="MAGETWO-26173"> <variation name="UpdateCmsPageRewriteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="cmsPageRewrite/dataset" xsi:type="string">cms_default_no_redirect</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/%default%</data> <data name="urlRewrite/data/request_path" xsi:type="string">request_path%isolation%</data> @@ -18,7 +18,7 @@ <constraint name="Magento\Cms\Test\Constraint\AssertUrlRewriteCmsPageRedirect" /> </variation> <variation name="UpdateCmsPageRewriteEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="cmsPageRewrite/dataset" xsi:type="string">cms_default_temporary_redirect</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">request_path%isolation%.html</data> @@ -28,7 +28,7 @@ <constraint name="Magento\Cms\Test\Constraint\AssertUrlRewriteCmsPageRedirect" /> </variation> <variation name="UpdateCmsPageRewriteEntityTestVariation3"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="cmsPageRewrite/dataset" xsi:type="string">cms_default_permanent_redirect</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">request_path%isolation%.htm</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php index 61166339475b7..dc1e901a3feae 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php @@ -29,7 +29,7 @@ class CustomerForm extends Form * * @var string */ - protected $customerAttribute = "[orig-name='%s[]']"; + protected $customerAttribute = "[name='%s[]']"; /** * Validation text message for a field. diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.php index 496b7a280ca18..de71cdff7ae4c 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/CreateDownloadableProductEntityTest.php @@ -11,6 +11,7 @@ use Magento\Catalog\Test\Page\Adminhtml\CatalogProductNew; use Magento\Downloadable\Test\Fixture\DownloadableProduct; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Steps: @@ -53,6 +54,13 @@ class CreateDownloadableProductEntityTest extends Injectable */ protected $catalogProductNew; + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Persist category * @@ -73,16 +81,19 @@ public function __prepare(Category $category) * @param Category $category * @param CatalogProductIndex $catalogProductIndexNewPage * @param CatalogProductNew $catalogProductNewPage + * @param EnvWhitelist $envWhitelist * @return void */ public function __inject( Category $category, CatalogProductIndex $catalogProductIndexNewPage, - CatalogProductNew $catalogProductNewPage + CatalogProductNew $catalogProductNewPage, + EnvWhitelist $envWhitelist ) { $this->category = $category; $this->catalogProductIndex = $catalogProductIndexNewPage; $this->catalogProductNew = $catalogProductNewPage; + $this->envWhitelist = $envWhitelist; } /** @@ -95,10 +106,21 @@ public function __inject( public function test(DownloadableProduct $product, Category $category) { // Steps + $this->envWhitelist->addHost('example.com'); $this->catalogProductIndex->open(); $this->catalogProductIndex->getGridPageActionBlock()->addProduct('downloadable'); $productBlockForm = $this->catalogProductNew->getProductForm(); $productBlockForm->fill($product, null, $category); $this->catalogProductNew->getFormPageActions()->save(); } + + /** + * Clean data after running test. + * + * @return void + */ + protected function tearDown() + { + $this->envWhitelist->removeHost('example.com'); + } } diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php index 59b1c7570c3de..363614825e568 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php @@ -16,7 +16,8 @@ class AssertExportSubmittedMessage extends AbstractConstraint /** * Text value to be checked. */ - const MESSAGE = 'Message is added to queue, wait to get your file soon'; + const MESSAGE = 'Message is added to queue, wait to get your file soon.' + . ' Make sure your cron job is running to export the file'; /** * Assert that export submitted message is visible after exporting. diff --git a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml index 86906d4c09406..ed0c4119dd825 100644 --- a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml +++ b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml @@ -30,7 +30,7 @@ <variation name="InstallTestVariation3" firstConstraint="Magento\Install\Test\Constraint\AssertSuccessInstall" summary="Install with table prefix"> <data name="user/dataset" xsi:type="string">default</data> <data name="install/dbTablePrefix" xsi:type="string">pref_</data> - <data name="install/storeLanguage" xsi:type="string">Chinese (China)</data> + <data name="install/storeLanguage" xsi:type="string">Chinese</data> <constraint name="Magento\Install\Test\Constraint\AssertSuccessInstall" next="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" prev="Magento\Install\Test\Constraint\AssertSuccessInstall" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/DeleteIntegrationEntityTest.xml b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/DeleteIntegrationEntityTest.xml index 607c0abf4302e..a43b88469faae 100644 --- a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/DeleteIntegrationEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/DeleteIntegrationEntityTest.xml @@ -11,6 +11,7 @@ <data name="integration/dataset" xsi:type="string">default</data> <constraint name="Magento\Integration\Test\Constraint\AssertIntegrationSuccessDeleteMessage" /> <constraint name="Magento\Integration\Test\Constraint\AssertIntegrationNotInGrid" /> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml index 9c9c917f8a66d..48129ef287498 100644 --- a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml @@ -94,7 +94,9 @@ <constraint name="Magento\LayeredNavigation\Test\Constraint\AssertFilterProductList" /> </variation> <variation name="FilterProductListTestVariation4" summary="Use sorting category filter when layered navigation is applied" ticketId="MAGETWO-42701"> + <data name="tag" xsi:type="string">test_type:mysql_search</data> <data name="configData" xsi:type="string">layered_navigation_manual_range_10</data> + <data name="runReindex" xsi:type="boolean">true</data> <data name="category/dataset" xsi:type="string">default_anchor_subcategory</data> <data name="category/data/category_products/dataset" xsi:type="string">catalogProductSimple::product_10_dollar, catalogProductSimple::product_20_dollar, configurableProduct::filterable_two_options_with_zero_price</data> <data name="layeredNavigation" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml b/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml index 4d2acc76c8703..c1970955013e8 100644 --- a/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml +++ b/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml @@ -11,7 +11,7 @@ <selector>input[name='start_at']</selector> </queue_start_at> <stores> - <selector>select[name="stores[0]"]</selector> + <selector>select[name="stores[]"]</selector> <input>multiselectgrouplist</input> </stores> <newsletter_subject> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml index 51809448e4edb..d66c3b702f076 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml @@ -29,7 +29,7 @@ <input>select</input> </price_rule_type> <order_statuses> - <selector>[name="order_statuses[0]"]</selector> + <selector>[name="order_statuses[]"]</selector> <input>multiselect</input> </order_statuses> <rules_list> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml index 5820de6772e1c..08e783e1329a4 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml @@ -23,7 +23,7 @@ <input>select</input> </show_order_statuses> <order_statuses> - <selector>[name="order_statuses[0]"]</selector> + <selector>[name="order_statuses[]"]</selector> <input>multiselect</input> </order_statuses> <show_empty_rows> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml index 504ce64bf2a73..3e1a1c727c668 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml @@ -12,7 +12,7 @@ <strategy>css selector</strategy> <fields> <stores> - <selector>[name="stores[0]"]</selector> + <selector>[name="stores[]"]</selector> <input>multiselectgrouplist</input> </stores> <is_active> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php index e7dd72d1d426c..da5e7101e4b33 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php @@ -102,7 +102,7 @@ public function test($gridActions, $gridStatus) $this->reviewIndex->getReviewGrid()->massaction( [['title' => $this->review->getTitle()]], [$gridActions => $gridStatus], - ($gridActions == 'Delete' ? true : false) + ($gridActions == 'Delete') ); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php index a5c172da3a992..4d37ebe95a7ec 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Invoice/Grid.php @@ -24,10 +24,10 @@ class Grid extends \Magento\Ui\Test\Block\Adminhtml\DataGrid 'selector' => 'input[name="order_increment_id"]', ], 'grand_total_from' => [ - 'selector' => 'input[name="base_grand_total[from]"]', + 'selector' => 'input[name="grand_total[from]"]', ], 'grand_total_to' => [ - 'selector' => 'input[name="base_grand_total[to]"]', + 'selector' => 'input[name="grand_total[to]"]', ], ]; diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php index 28bb00757dac1..8aff098fb9c3e 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Creditmemo/Totals.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\Block\Adminhtml\Order\Creditmemo; use Magento\Mtf\Client\Locator; diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml index 294f64966bde9..d868798eba79d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml @@ -26,7 +26,7 @@ <input>select</input> </show_order_statuses> <order_statuses> - <selector>[name="order_statuses[0]"]</selector> + <selector>[name="order_statuses[]"]</selector> <input>multiselect</input> </order_statuses> <show_actual_columns> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml index c4e03b94d2ada..b1e3b9a9d9f1e 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\CreateOrderBackendTest" summary="Create Order from Admin within Offline Payment Methods" ticketId="MAGETWO-28696"> <variation name="CreateOrderBackendTestVariation18" summary="Create order with condition available product qty = ordered product qty" ticketId="MAGETWO-12798"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products/0" xsi:type="string">catalogProductSimple::product_with_qty_25</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="billingAddress/dataset" xsi:type="string">US_address_1_without_email</data> @@ -24,6 +25,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductsOutOfStock" /> </variation> <variation name="CreateOrderBackendTestVariation19" summary="'Reorder' button is not visible for customer if ordered item is out of stock" ticketId="MAGETWO-63924"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products/0" xsi:type="string">catalogProductSimple::default_qty_1</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="billingAddress/dataset" xsi:type="string">US_address_1_without_email</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveRecentlyComparedProductsOnOrderPageTest.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveRecentlyComparedProductsOnOrderPageTest.php index 4dce560693ab3..cf93da5e9f6fb 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveRecentlyComparedProductsOnOrderPageTest.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveRecentlyComparedProductsOnOrderPageTest.php @@ -8,10 +8,13 @@ use Magento\Catalog\Test\Page\Product\CatalogProductCompare; use Magento\Catalog\Test\Page\Product\CatalogProductView; +use Magento\Catalog\Test\TestStep\CreateProductsStep; use Magento\Cms\Test\Page\CmsIndex; use Magento\Customer\Test\Fixture\Customer; use Magento\Customer\Test\Page\Adminhtml\CustomerIndex; use Magento\Customer\Test\Page\Adminhtml\CustomerIndexEdit; +use Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep; +use Magento\Mtf\Util\Command\Cli\Config; use Magento\Sales\Test\Page\Adminhtml\OrderCreateIndex; use Magento\Mtf\Client\BrowserInterface; use Magento\Mtf\TestCase\Injectable; @@ -91,21 +94,27 @@ class MoveRecentlyComparedProductsOnOrderPageTest extends Injectable */ protected $catalogProductCompare; + /** + * @var Config + */ + private $config; + /** * Create customer. - * * @param Customer $customer * @param BrowserInterface $browser + * @param Config $config * @return array */ - public function __prepare(Customer $customer, BrowserInterface $browser) + public function __prepare(Customer $customer, BrowserInterface $browser, Config $config) { $customer->persist(); // Login under customer $this->objectManager - ->create(\Magento\Customer\Test\TestStep\LoginCustomerOnFrontendStep::class, ['customer' => $customer]) + ->create(LoginCustomerOnFrontendStep::class, ['customer' => $customer]) ->run(); $this->browser = $browser; + $this->config = $config; return ['customer' => $customer]; } @@ -137,20 +146,26 @@ public function __inject( $this->catalogProductCompare = $catalogProductCompare; } + public function setUp() + { + $this->config->setConfig('reports/options/enabled', 1); + parent::setUp(); + } + /** * Move recently compared products on order page. - * * @param Customer $customer * @param string $products * @param bool $productsIsConfigured * @return array + * @throws \Exception */ public function test(Customer $customer, $products, $productsIsConfigured = false) { // Preconditions // Create product $products = $this->objectManager->create( - \Magento\Catalog\Test\TestStep\CreateProductsStep::class, + CreateProductsStep::class, ['products' => $products] )->run()['products']; foreach ($products as $itemProduct) { @@ -171,4 +186,10 @@ public function test(Customer $customer, $products, $productsIsConfigured = fals return ['products' => $products, 'productsIsConfigured' => $productsIsConfigured]; } + + public function tearDown() + { + $this->config->setConfig('reports/options/enabled', 0); + parent::tearDown(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/PrintOrderFrontendGuestTest.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/PrintOrderFrontendGuestTest.php index 01e43defde814..9eb13734531d9 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/PrintOrderFrontendGuestTest.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/PrintOrderFrontendGuestTest.php @@ -8,6 +8,7 @@ use Magento\Mtf\Client\BrowserInterface; use Magento\Mtf\TestCase\Scenario; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Preconditions: @@ -41,14 +42,25 @@ class PrintOrderFrontendGuestTest extends Scenario */ protected $browser; + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Prepare data. * * @param BrowserInterface $browser + * @param EnvWhitelist $envWhitelist */ - public function __prepare(BrowserInterface $browser) - { + public function __prepare( + BrowserInterface $browser, + EnvWhitelist $envWhitelist + ) { $this->browser = $browser; + $this->envWhitelist = $envWhitelist; } /** @@ -58,6 +70,7 @@ public function __prepare(BrowserInterface $browser) */ public function test() { + $this->envWhitelist->addHost('example.com'); $this->executeScenario(); } @@ -68,6 +81,7 @@ public function test() */ public function tearDown() { + $this->envWhitelist->removeHost('example.com'); $this->browser->closeWindow(); } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php index 25b576a06dd3f..6ef1713c8542f 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestStep/CreateCreditMemoStep.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\TestStep; use Magento\Checkout\Test\Fixture\Cart; @@ -100,7 +102,6 @@ public function run() if ($hasChangeTotals) { $this->orderCreditMemoNew->getTotalsBlock()->clickUpdateTotals(); } - $this->orderCreditMemoNew->getFormBlock()->submit(); } @@ -139,7 +140,7 @@ private function isTotalsDataChanged(array $data): bool ]; foreach ($compareData as $fieldName => $fieldValue) { - if (isset($data['form_data'][$fieldName]) && $fieldValue != $data['form_data'][$fieldName]) { + if (isset($data['form_data'][$fieldName]) && $fieldValue !== $data['form_data'][$fieldName]) { return true; } } diff --git a/dev/tests/functional/tests/app/Magento/Setup/Test/TestCase/UpgradeSystemTest.php b/dev/tests/functional/tests/app/Magento/Setup/Test/TestCase/UpgradeSystemTest.php index c9b3ce35e84ed..7a82b43dba76d 100644 --- a/dev/tests/functional/tests/app/Magento/Setup/Test/TestCase/UpgradeSystemTest.php +++ b/dev/tests/functional/tests/app/Magento/Setup/Test/TestCase/UpgradeSystemTest.php @@ -132,6 +132,8 @@ public function test( // Check application version $this->adminDashboard->open(); + $this->adminDashboard->getModalMessage()->dismissIfModalAppears(); + $this->adminDashboard->getModalMessage()->waitModalWindowToDisappear(); $assertApplicationVersion->processAssert($this->adminDashboard, $version); } } diff --git a/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/CreateShipmentEntityTest.xml b/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/CreateShipmentEntityTest.xml index 06acf95effdbf..3fa93602e5256 100644 --- a/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/CreateShipmentEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Shipping/Test/TestCase/CreateShipmentEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Shipping\Test\TestCase\CreateShipmentEntityTest" summary="Create Shipment for Offline Payment Methods" ticketId="MAGETWO-28708"> <variation name="CreateShipmentEntityTestVariation1" summary="Shipment with tracking number"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test,mftf_migrated:yes</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/entity_id/products" xsi:type="string">catalogProductSimple::default</data> <data name="order/data/total_qty_ordered/0" xsi:type="string">1</data> @@ -38,6 +38,7 @@ <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentInShipmentsGrid" /> <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentItems" /> <constraint name="Magento\Shipping\Test\Constraint\AssertShipTotalQuantity" /> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/Modal.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/Modal.php index 5c27776c09620..eb949dccb7ffc 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/Modal.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/Modal.php @@ -163,6 +163,30 @@ function () { ); } + /** + * Dismiss the modal if it appears + * + * @return void + */ + public function dismissIfModalAppears() + { + $browser = $this->browser; + $selector = $this->dismissWarningSelector; + $browser->waitUntil( + function () use ($browser, $selector) { + $item = $browser->find($selector); + if ($item->isVisible()) { + return true; + } + $this->waitModalAnimationFinished(); + return true; + } + ); + if ($this->browser->find($selector)->isVisible()) { + $this->browser->find($selector)->click(); + } + } + /** * Waiting until CSS animation is done. * Transition-duration is set at this file: "<magento_root>/lib/web/css/source/components/_modals.less" diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCategoryUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCategoryUrlRewriteEntityTest.xml index 56440e8a8492b..42f71b8d01f76 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCategoryUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCategoryUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\DeleteCategoryUrlRewriteEntityTest" summary="Delete Category URL Rewrites" ticketId="MAGETWO-25086"> <variation name="DeleteCategoryUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">catalog/category/view/id/%category::default%</data> <data name="urlRewrite/data/redirect_type" xsi:type="string">No</data> <data name="urlRewrite/data/request_path" xsi:type="string">-</data> @@ -15,6 +16,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertPageByUrlRewriteIsNotFound" /> </variation> <variation name="DeleteCategoryUrlRewriteEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">catalog/category/view/id/%category::default%</data> <data name="urlRewrite/data/redirect_type" xsi:type="string">No</data> <data name="urlRewrite/data/request_path" xsi:type="string">example%isolation%.html</data> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteAdminUserEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteAdminUserEntityTest.php index 42914cd697bfd..81d9fe8393aee 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteAdminUserEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteAdminUserEntityTest.php @@ -117,6 +117,8 @@ public function testDeleteAdminUserEntity( $this->adminAuthLogin->open(); $this->adminAuthLogin->getLoginBlock()->fill($user); $this->adminAuthLogin->getLoginBlock()->submit(); + $this->adminAuthLogin->waitForHeaderBlock(); + $this->adminAuthLogin->dismissAdminUsageNotification(); } $this->userIndex->open(); $this->userIndex->getUserGrid()->searchAndOpen($filter); diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteUserRoleEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteUserRoleEntityTest.php index 47c5a4095830d..2a1c9feb440b9 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteUserRoleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/DeleteUserRoleEntityTest.php @@ -117,6 +117,8 @@ public function testDeleteAdminUserRole( $this->adminAuthLogin->open(); $this->adminAuthLogin->getLoginBlock()->fill($adminUser); $this->adminAuthLogin->getLoginBlock()->submit(); + $this->adminAuthLogin->waitForHeaderBlock(); + $this->adminAuthLogin->dismissAdminUsageNotification(); } $this->userRoleIndex->open(); $this->userRoleIndex->getRoleGrid()->searchAndOpen($filter); diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml index 052197e3db33c..f89f94ba03e73 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/LockAdminUserEntityTest.xml @@ -24,6 +24,7 @@ <data name="incorrectPassword" xsi:type="string">honey boo boo</data> <data name="attempts" xsi:type="string">7</data> <constraint name="Magento\User\Test\Constraint\AssertUserFailedLoginMessage" /> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.php index e8869de13462a..dab4cec22c86d 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.php @@ -147,6 +147,8 @@ public function testUpdateAdminUser( $this->adminAuth->open(); $this->adminAuth->getLoginBlock()->fill($initialUser); $this->adminAuth->getLoginBlock()->submit(); + $this->adminAuth->waitForHeaderBlock(); + $this->adminAuth->dismissAdminUsageNotification(); } $this->userIndex->open(); $this->userIndex->getUserGrid()->searchAndOpen($filter); diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml index a89d1ede80112..ac99c2d8721f3 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\User\Test\TestCase\UpdateAdminUserEntityTest" summary="Update Admin User" ticketId="MAGETWO-24345"> <variation name="UpdateAdminUserEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S3</data> + <data name="tag" xsi:type="string">severity:S3, mftf_migrated:yes</data> <data name="initialUser/dataset" xsi:type="string">custom_admin_with_default_role</data> <data name="user/data/role_id/dataset" xsi:type="string">role::role_sales</data> <data name="user/data/current_password" xsi:type="string">123123q</data> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php index 58450abc71633..c7031e9fccbeb 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php @@ -102,6 +102,8 @@ public function testUpdateAdminUserRolesEntity( $this->adminAuthLogin->open(); $this->adminAuthLogin->getLoginBlock()->fill($user); $this->adminAuthLogin->getLoginBlock()->submit(); + $this->adminAuthLogin->waitForHeaderBlock(); + $this->adminAuthLogin->dismissAdminUsageNotification(); $this->rolePage->open(); $this->rolePage->getRoleGrid()->searchAndOpen($filter); $this->userRoleEditRole->getRoleFormTabs()->fill($role); diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php index c244e27d42899..3e2a3a3d59fc1 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php @@ -119,6 +119,8 @@ private function login() { $this->adminAuth->getLoginBlock()->fill($this->user); $this->adminAuth->getLoginBlock()->submit(); + $this->adminAuth->waitForHeaderBlock(); + $this->adminAuth->dismissAdminUsageNotification(); $this->adminAuth->getLoginBlock()->waitFormNotVisible(); } } diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/Constraint/AssertWidgetRecentlyComparedProducts.php b/dev/tests/functional/tests/app/Magento/Widget/Test/Constraint/AssertWidgetRecentlyComparedProducts.php index 5a67fe43a691b..bf70f0f901352 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/Constraint/AssertWidgetRecentlyComparedProducts.php +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/Constraint/AssertWidgetRecentlyComparedProducts.php @@ -89,8 +89,27 @@ public function processAssert( $this->addProducts($products); $this->removeCompareProducts(); + $cmsIndex->open(); + //Widgets data is cache via LocalStorage so it might take couple of refreshes before cache is invalidated. + $refreshCount = 3; + $refreshNo = 1; + $isVisible = false; + while (!$isVisible && $refreshNo <= $refreshCount) { + $browser->refresh(); + try { + $isVisible = $browser->waitUntil( + function () use ($widget) { + return $this->catalogProductCompare->getWidgetView() + ->isWidgetVisible($widget, 'Recently Compared') ? true : null; + } + ); + } catch (\Throwable $exception) { + $isVisible = false; + } + $refreshNo++; + } \PHPUnit\Framework\Assert::assertTrue( - $this->catalogProductCompare->getWidgetView()->isWidgetVisible($widget, 'Recently Compared'), + $isVisible, 'Widget is absent on Product Compare page.' ); } diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.php index 9c5ffb9dd8013..c12b2d0991224 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.php @@ -7,6 +7,7 @@ namespace Magento\Wishlist\Test\TestCase; use Magento\Customer\Test\Fixture\Customer; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Test Flow: @@ -30,15 +31,26 @@ class AddProductToWishlistEntityTest extends AbstractWishlistTest const MVP = 'no'; /* end tags */ + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Prepare data for test * * @param Customer $customer + * @param EnvWhitelist $envWhitelist * @return array */ - public function __prepare(Customer $customer) - { + public function __prepare( + Customer $customer, + EnvWhitelist $envWhitelist + ) { $customer->persist(); + $this->envWhitelist = $envWhitelist; return ['customer' => $customer]; } @@ -53,6 +65,7 @@ public function __prepare(Customer $customer) */ public function test(Customer $customer, $product, $configure = true) { + $this->envWhitelist->addHost('example.com'); $product = $this->createProducts($product)[0]; // Steps: @@ -61,4 +74,14 @@ public function test(Customer $customer, $product, $configure = true) return ['product' => $product]; } + + /** + * Clean data after running test. + * + * @return void + */ + public function tearDown() + { + $this->envWhitelist->removeHost('example.com'); + } } diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ConfigureProductInCustomerWishlistOnBackendTest.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ConfigureProductInCustomerWishlistOnBackendTest.php index ee3bf77a1aa0d..27c60281660bb 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ConfigureProductInCustomerWishlistOnBackendTest.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ConfigureProductInCustomerWishlistOnBackendTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Test\Fixture\Customer; use Magento\Customer\Test\Page\Adminhtml\CustomerIndex; use Magento\Customer\Test\Page\Adminhtml\CustomerIndexEdit; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Preconditions: @@ -35,15 +36,26 @@ class ConfigureProductInCustomerWishlistOnBackendTest extends AbstractWishlistTe const MVP = 'no'; /* end tags */ + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Create customer. * * @param Customer $customer + * @param EnvWhitelist $envWhitelist * @return array */ - public function __prepare(Customer $customer) - { + public function __prepare( + Customer $customer, + EnvWhitelist $envWhitelist + ) { $customer->persist(); + $this->envWhitelist = $envWhitelist; return ['customer' => $customer]; } @@ -64,6 +76,7 @@ public function test( CustomerIndexEdit $customerIndexEdit ) { // Preconditions + $this->envWhitelist->addHost('example.com'); $product = $this->createProducts($product)[0]; $this->loginCustomer($customer); $this->addToWishlist([$product]); @@ -80,4 +93,14 @@ public function test( return['product' => $product]; } + + /** + * Clean data after running test. + * + * @return void + */ + public function tearDown() + { + $this->envWhitelist->removeHost('example.com'); + } } diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/DeleteProductsFromWishlistOnFrontendTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/DeleteProductsFromWishlistOnFrontendTest.xml index d6b930d999537..26593636d3fcb 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/DeleteProductsFromWishlistOnFrontendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/DeleteProductsFromWishlistOnFrontendTest.xml @@ -39,6 +39,7 @@ <constraint name="Magento\Wishlist\Test\Constraint\AssertWishlistIsEmpty" /> </variation> <variation name="DeleteProductsFromWishlistOnFrontendTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products/0" xsi:type="string">bundleProduct::bundle_dynamic_product</data> <data name="removedProductsIndex" xsi:type="array"> <item name="0" xsi:type="string">1</item> @@ -46,6 +47,7 @@ <constraint name="Magento\Wishlist\Test\Constraint\AssertWishlistIsEmpty" /> </variation> <variation name="DeleteProductsFromWishlistOnFrontendTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products/0" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="removedProductsIndex" xsi:type="array"> <item name="0" xsi:type="string">1</item> @@ -53,6 +55,7 @@ <constraint name="Magento\Wishlist\Test\Constraint\AssertWishlistIsEmpty" /> </variation> <variation name="DeleteProductsFromWishlistOnFrontendTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products/0" xsi:type="string">configurableProduct::default</data> <data name="removedProductsIndex" xsi:type="array"> <item name="0" xsi:type="string">1</item> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml index 95e6a854ed266..aa3b646161a17 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml @@ -15,6 +15,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertCartIsEmpty" /> </variation> <variation name="MoveProductFromShoppingCartToWishlistTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/0" xsi:type="string">catalogProductVirtual::default</data> <constraint name="Magento\Wishlist\Test\Constraint\AssertMoveProductToWishlistSuccessMessage" /> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist" /> @@ -29,7 +30,7 @@ <constraint name="Magento\Wishlist\Test\Constraint\AssertProductDetailsInWishlist" /> </variation> <variation name="MoveProductFromShoppingCartToWishlistTestVariation4"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/0" xsi:type="string">configurableProduct::default</data> <constraint name="Magento\Wishlist\Test\Constraint\AssertMoveProductToWishlistSuccessMessage" /> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist" /> @@ -37,6 +38,7 @@ <constraint name="Magento\Wishlist\Test\Constraint\AssertProductDetailsInWishlist" /> </variation> <variation name="MoveProductFromShoppingCartToWishlistTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/0" xsi:type="string">bundleProduct::bundle_dynamic_product</data> <constraint name="Magento\Wishlist\Test\Constraint\AssertMoveProductToWishlistSuccessMessage" /> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist" /> @@ -44,6 +46,7 @@ <constraint name="Magento\Wishlist\Test\Constraint\AssertProductDetailsInWishlist" /> </variation> <variation name="MoveProductFromShoppingCartToWishlistTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/0" xsi:type="string">bundleProduct::bundle_fixed_product</data> <constraint name="Magento\Wishlist\Test\Constraint\AssertMoveProductToWishlistSuccessMessage" /> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist" /> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ViewProductInCustomerWishlistOnBackendTest.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ViewProductInCustomerWishlistOnBackendTest.php index f81f87d5b6227..1bba73cdf5e9f 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ViewProductInCustomerWishlistOnBackendTest.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ViewProductInCustomerWishlistOnBackendTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Test\Fixture\Customer; use Magento\Customer\Test\Page\Adminhtml\CustomerIndex; use Magento\Customer\Test\Page\Adminhtml\CustomerIndexEdit; +use Magento\Mtf\Util\Command\Cli\EnvWhitelist; /** * Test Flow: @@ -34,15 +35,26 @@ class ViewProductInCustomerWishlistOnBackendTest extends AbstractWishlistTest const MVP = 'no'; /* end tags */ + /** + * DomainWhitelist CLI + * + * @var EnvWhitelist + */ + private $envWhitelist; + /** * Prepare customer for test. * * @param Customer $customer + * @param EnvWhitelist $envWhitelist * @return array */ - public function __prepare(Customer $customer) - { + public function __prepare( + Customer $customer, + EnvWhitelist $envWhitelist + ) { $customer->persist(); + $this->envWhitelist = $envWhitelist; return ['customer' => $customer]; } @@ -63,6 +75,7 @@ public function test( CustomerIndexEdit $customerIndexEdit ) { // Preconditions + $this->envWhitelist->addHost('example.com'); $product = $this->createProducts($product)[0]; $this->loginCustomer($customer); $this->addToWishlist([$product], true); @@ -74,4 +87,14 @@ public function test( return['product' => $product]; } + + /** + * Clean data after running test. + * + * @return void + */ + public function tearDown() + { + $this->envWhitelist->removeHost('example.com'); + } } diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/acceptance.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/acceptance.xml index 6a8e2c615f847..2eae769416c29 100644 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/acceptance.xml +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/acceptance.xml @@ -13,6 +13,7 @@ </allow> <deny> <tag group="stable" value="no" /> + <tag group="mftf_migrated" value="yes" /> </deny> </rule> <rule scope="variation"> @@ -21,6 +22,7 @@ </allow> <deny> <tag group="stable" value="no" /> + <tag group="mftf_migrated" value="yes" /> </deny> </rule> </config> diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml index 17578fcfe85b1..6141151518332 100644 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml @@ -22,7 +22,7 @@ </rule> <rule scope="variation"> <deny> - <tag group="test_type" value="3rd_party_test, 3rd_party_test_single_flow" /> + <tag group="test_type" value="3rd_party_test, 3rd_party_test_single_flow, mysql_search" /> <tag group="stable" value="no" /> <tag group="mftf_migrated" value="yes" /> <tag group="to_maintain" value="yes" /> diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/mysql_search.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/mysql_search.xml new file mode 100644 index 0000000000000..17578fcfe85b1 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/mysql_search.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="../../../../../vendor/magento/mtf/Magento/Mtf/TestRunner/etc/testRunner.xsd"> + <rule scope="testcase"> + <deny> + <tag group="stable" value="no" /> + <tag group="to_maintain" value="yes" /> + <tag group="mftf_migrated" value="yes" /> + </deny> + </rule> + <rule scope="testsuite"> + <deny> + <module value="Magento_Setup" strict="1" /> + <module value="Magento_SampleData" strict="1" /> + </deny> + </rule> + <rule scope="variation"> + <deny> + <tag group="test_type" value="3rd_party_test, 3rd_party_test_single_flow" /> + <tag group="stable" value="no" /> + <tag group="mftf_migrated" value="yes" /> + <tag group="to_maintain" value="yes" /> + </deny> + </rule> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/etc/module.xml new file mode 100644 index 0000000000000..bae0739d237e1 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/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_TestModuleCatalogSearch"> + <sequence> + <module name="Magento_CatalogSearch"/> + </sequence> + </module> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/registration.php b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/registration.php new file mode 100644 index 0000000000000..78fb97a9e1134 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/composer.json b/dev/tests/integration/_files/Magento/TestModuleCspConfig/composer.json new file mode 100644 index 0000000000000..f4d4075fe3377 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-csp-config", + "description": "test csp module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleCspConfig" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..e9eb1fe21aa4f --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp/etc/csp_whitelist.xsd"> + <policies> + <policy id="object-src"> + <values> + <value id="mage-base" type="host">https://magento.com</value> + <value id="hash" type="hash" algorithm="sha256">B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=</value> + </values> + </policy> + <policy id="media-src"> + <values> + <value id="mage-base" type="host">https://magento.com</value> + <value id="devdocs-base" type="host">https://devdocs.magento.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/module.xml new file mode 100644 index 0000000000000..ee90595cacf19 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/module.xml @@ -0,0 +1,10 @@ +<?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_TestModuleCspConfig" active="true" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/registration.php b/dev/tests/integration/_files/Magento/TestModuleCspConfig/registration.php new file mode 100644 index 0000000000000..9749bf5a7f621 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCspConfig') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCspConfig', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig2/composer.json b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/composer.json new file mode 100644 index 0000000000000..ebf1d57fe6593 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-csp-config2", + "description": "test csp module 2", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleCspConfig2" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig2/etc/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..eff59271bf422 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/etc/csp_whitelist.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp/etc/csp_whitelist.xsd"> + <policies> + <policy id="object-src"> + <values> + <value id="devdocs-base" type="host">https://devdocs.magento.com</value> + <value id="mage-base" type="host">http://magento.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig2/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/etc/module.xml new file mode 100644 index 0000000000000..de3c2ea25f4f2 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/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_TestModuleCspConfig2" active="true"> + <sequence> + <module name="Magento_TestModuleCspConfig"/> + </sequence> + </module> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig2/registration.php b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/registration.php new file mode 100644 index 0000000000000..539d9cd20e10f --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig2/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCspConfig2') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCspConfig2', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/FooFilter.php b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/FooFilter.php new file mode 100644 index 0000000000000..9cdf909f41823 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/FooFilter.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestModuleSimpleTemplateDirective\Model; + +use Magento\Framework\Filter\SimpleDirective\ProcessorInterface; +use Magento\Framework\Filter\Template; + +/** + * Filters a value for testing purposes + */ +class FooFilter implements \Magento\Framework\Filter\DirectiveProcessor\FilterInterface +{ + /** + * @inheritDoc + */ + public function filterValue(string $value, array $params): string + { + $arg1 = $params[0] ?? null; + + return strtoupper(strrev($value . $arg1 ?? '')); + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return 'foofilter'; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/LegacyFilter.php b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/LegacyFilter.php new file mode 100644 index 0000000000000..2f45183585dec --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/LegacyFilter.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestModuleSimpleTemplateDirective\Model; + +use Magento\Framework\Filter\Template; + +/** + * A legacy directive test entity + */ +class LegacyFilter extends Template +{ + /** + * Filter a directive + * + * @param $construction + * @return string + */ + protected function coolDirective($construction) + { + return 'value1: ' . $construction[1] . ':' . $construction[2]; + } + + /** + * Filter a directive + * + * @param $construction + * @return string + */ + public function coolerDirective($construction) + { + return 'value2: ' . $construction[1] . ':' . $construction[2]; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/MyDirProcessor.php b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/MyDirProcessor.php new file mode 100644 index 0000000000000..faf4728f3c338 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/Model/MyDirProcessor.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestModuleSimpleTemplateDirective\Model; + +use Magento\Framework\Filter\SimpleDirective\ProcessorInterface; +use Magento\Framework\Filter\Template; + +/** + * Handles the {{mydir}} directive + */ +class MyDirProcessor implements ProcessorInterface +{ + /** + * @inheritDoc + */ + public function getName(): string + { + return 'mydir'; + } + + /** + * @inheritDoc + */ + public function process( + $value, + array $parameters, + ?string $html + ): string { + return $value . $parameters['param1'] . $html; + } + + /** + * @inheritDoc + */ + public function getDefaultFilters(): ?array + { + return ['foofilter']; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/etc/di.xml b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/etc/di.xml new file mode 100644 index 0000000000000..e102c28c7307d --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/etc/di.xml @@ -0,0 +1,23 @@ +<?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\Filter\SimpleDirective\ProcessorPool"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="mydir" xsi:type="object">Magento\TestModuleSimpleTemplateDirective\Model\MyDirProcessor</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\Filter\DirectiveProcessor\Filter\FilterPool"> + <arguments> + <argument name="filters" xsi:type="array"> + <item name="foofilter" xsi:type="object">Magento\TestModuleSimpleTemplateDirective\Model\FooFilter</item> + </argument> + </arguments> + </type> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/etc/module.xml new file mode 100644 index 0000000000000..30f1c3f08cbfe --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/etc/module.xml @@ -0,0 +1,10 @@ +<?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_TestModuleSimpleTemplateDirective" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/registration.php b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/registration.php new file mode 100644 index 0000000000000..4b1c200857b90 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSimpleTemplateDirective/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleSimpleTemplateDirective') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleSimpleTemplateDirective', __DIR__); +} diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index b8264ea977750..2770283637dc7 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -31,4 +31,10 @@ \Magento\TestFramework\Lock\Backend\DummyLocker::class, \Magento\Framework\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::class, \Magento\Framework\HTTP\AsyncClientInterface::class => \Magento\TestFramework\HTTP\AsyncClientInterfaceMock::class, + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class, + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class, + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class ]; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/CategoryLayoutUpdateManager.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/CategoryLayoutUpdateManager.php new file mode 100644 index 0000000000000..48ff3a6496722 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/CategoryLayoutUpdateManager.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; + +/** + * Easy way to fake available files. + */ +class CategoryLayoutUpdateManager extends LayoutUpdateManager +{ + /** + * @var array Keys are category IDs, values - file names. + */ + private $fakeFiles = []; + + /** + * Supply fake files for a category. + * + * @param int $forCategoryId + * @param string[]|null $files Pass null to reset. + */ + public function setCategoryFakeFiles(int $forCategoryId, ?array $files): void + { + if ($files === null) { + unset($this->fakeFiles[$forCategoryId]); + } else { + $this->fakeFiles[$forCategoryId] = $files; + } + } + + /** + * @inheritDoc + */ + public function fetchAvailableFiles(CategoryInterface $category): array + { + if (array_key_exists($category->getId(), $this->fakeFiles)) { + return $this->fakeFiles[$category->getId()]; + } + + return parent::fetchAvailableFiles($category); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Attribute/DataProvider/MediaImage.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Attribute/DataProvider/MediaImage.php new file mode 100644 index 0000000000000..dd706ab29c326 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Attribute/DataProvider/MediaImage.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Attribute\DataProvider; + +use Magento\TestFramework\Eav\Model\Attribute\DataProvider\AbstractBaseAttributeData; + +/** + * Product attribute data for attribute with input type media image. + */ +class MediaImage extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + $result = parent::getAttributeData(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + $result = parent::getAttributeDataWithCheckArray(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'media_image'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Attribute/DataProvider/Price.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Attribute/DataProvider/Price.php new file mode 100644 index 0000000000000..04ee6bb0a5740 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Attribute/DataProvider/Price.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Attribute\DataProvider; + +use Magento\TestFramework\Eav\Model\Attribute\DataProvider\AbstractBaseAttributeData; + +/** + * Product attribute data for attribute with input type weee. + */ +class Price extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['is_filterable'] = '0'; + $this->defaultAttributePostData['is_filterable_in_search'] = '0'; + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + $result = parent::getAttributeData(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + $result = parent::getAttributeDataWithCheckArray(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'price'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractBase.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractBase.php new file mode 100644 index 0000000000000..36a4f6662cb09 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractBase.php @@ -0,0 +1,172 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +/** + * Base custom options data provider. + */ +abstract class AbstractBase +{ + /** + * Return data for create options for all cases. + * + * @return array + */ + public function getDataForCreateOptions(): array + { + return [ + "type_{$this->getType()}_title" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_required_options" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_not_required_options" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_options_with_fixed_price" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_options_with_percent_price" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'percent', + ], + ], + "type_{$this->getType()}_price" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 22, + 'price_type' => 'percent', + ], + ], + "type_{$this->getType()}_sku" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 22, + 'price_type' => 'percent', + ], + ], + ]; + } + + /** + * Return data for create options for all cases. + * + * @return array + */ + public function getDataForUpdateOptions(): array + { + return array_merge_recursive( + $this->getDataForCreateOptions(), + [ + "type_{$this->getType()}_title" => [ + [ + 'title' => 'Test updated option title', + ] + ], + "type_{$this->getType()}_required_options" => [ + [ + 'is_require' => 0, + ], + ], + "type_{$this->getType()}_not_required_options" => [ + [ + 'is_require' => 1, + ], + ], + "type_{$this->getType()}_options_with_fixed_price" => [ + [ + 'price_type' => 'percent', + ], + ], + "type_{$this->getType()}_options_with_percent_price" => [ + [ + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_price" => [ + [ + 'price' => 60, + ], + ], + "type_{$this->getType()}_sku" => [ + [ + 'sku' => 'Updated option sku', + ], + ], + ] + ); + } + + /** + * Return option type. + * + * @return string + */ + abstract protected function getType(): string; +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractSelect.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractSelect.php new file mode 100644 index 0000000000000..0cbe64bf1d965 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractSelect.php @@ -0,0 +1,199 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; + +/** + * Abstract data provider for options from select group. + */ +abstract class AbstractSelect extends AbstractBase +{ + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getDataForCreateOptions(): array + { + return [ + "type_{$this->getType()}_title" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + "type_{$this->getType()}_required_options" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + "type_{$this->getType()}_not_required_options" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + "type_{$this->getType()}_options_with_fixed_price" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + "type_{$this->getType()}_options_with_percent_price" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + "type_{$this->getType()}_price" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 22, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + "type_{$this->getType()}_sku" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + ]; + } + + /** + * @inheritdoc + */ + public function getDataForUpdateOptions(): array + { + return array_merge_recursive( + $this->getDataForCreateOptions(), + [ + "type_{$this->getType()}_title" => [ + [ + 'title' => 'Updated test option title 1', + ], + [], + ], + "type_{$this->getType()}_required_options" => [ + [ + 'is_require' => 0, + ], + [], + ], + "type_{$this->getType()}_not_required_options" => [ + [ + 'is_require' => 1, + ], + [], + ], + "type_{$this->getType()}_options_with_fixed_price" => [ + [], + [ + 'price_type' => 'percent', + ], + ], + "type_{$this->getType()}_options_with_percent_price" => [ + [], + [ + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_price" => [ + [], + [ + 'price' => 666, + ], + ], + "type_{$this->getType()}_sku" => [ + [], + [ + 'sku' => 'updated-test-option-1-value-1', + ], + ], + ] + ); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractText.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractText.php new file mode 100644 index 0000000000000..42028eee632be --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/AbstractText.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; + +/** + * Abstract data provider for options from text group. + */ +abstract class AbstractText extends AbstractBase +{ + /** + * @inheritdoc + */ + public function getDataForCreateOptions(): array + { + return array_merge_recursive( + parent::getDataForCreateOptions(), + [ + "type_{$this->getType()}_options_with_max_charters_configuration" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + "type_{$this->getType()}_options_without_max_charters_configuration" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + public function getDataForUpdateOptions(): array + { + return array_merge_recursive( + parent::getDataForUpdateOptions(), + [ + "type_{$this->getType()}_options_with_max_charters_configuration" => [ + [ + 'max_characters' => 0, + ], + ], + "type_{$this->getType()}_options_without_max_charters_configuration" => [ + [ + 'max_characters' => 55, + ], + ], + ] + ); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Area.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Area.php new file mode 100644 index 0000000000000..137e77412f7b9 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Area.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractText; + +/** + * Data provider for custom options from text group with type "area". + */ +class Area extends AbstractText +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_AREA; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Checkbox.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Checkbox.php new file mode 100644 index 0000000000000..9a5f57d30946b --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Checkbox.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractSelect; + +/** + * Data provider for custom options from select group with type "Checkbox". + */ +class Checkbox extends AbstractSelect +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Date.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Date.php new file mode 100644 index 0000000000000..501265849be93 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Date.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; + +/** + * Data provider for custom options from date group with type "date". + */ +class Date extends AbstractBase +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_DATE; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/DateTime.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/DateTime.php new file mode 100644 index 0000000000000..e3e154cc386e0 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/DateTime.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; + +/** + * Data provider for custom options from date group with type "date & time". + */ +class DateTime extends AbstractBase +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/DropDown.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/DropDown.php new file mode 100644 index 0000000000000..2c8b9f2f25dc6 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/DropDown.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractSelect; + +/** + * Data provider for custom options from select group with type "Drop-down". + */ +class DropDown extends AbstractSelect +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Field.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Field.php new file mode 100644 index 0000000000000..12df838bd46c2 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Field.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractText; + +/** + * Data provider for custom options from text group with type "field". + */ +class Field extends AbstractText +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_FIELD; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php new file mode 100644 index 0000000000000..35f449a404410 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; + +/** + * Data provider for options from file group with type "file". + */ +class File extends AbstractBase +{ + /** + * @inheritdoc + */ + public function getDataForCreateOptions(): array + { + return array_merge_recursive( + parent::getDataForCreateOptions(), + [ + "type_{$this->getType()}_option_file_extension" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + 'file_extension' => 'gif', + 'image_size_x' => 10, + 'image_size_y' => 20, + ], + ], + "type_{$this->getType()}_option_maximum_file_size" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + 'file_extension' => 'gif', + 'image_size_x' => 10, + 'image_size_y' => 20, + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + public function getDataForUpdateOptions(): array + { + return array_merge_recursive( + parent::getDataForUpdateOptions(), + [ + "type_{$this->getType()}_option_file_extension" => [ + [ + 'file_extension' => 'jpg', + ], + ], + "type_{$this->getType()}_option_maximum_file_size" => [ + [ + 'image_size_x' => 300, + 'image_size_y' => 815, + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_FILE; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/MultipleSelect.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/MultipleSelect.php new file mode 100644 index 0000000000000..3e958b1ac39a0 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/MultipleSelect.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractSelect; + +/** + * Data provider for custom options from select group with type "Drop-down". + */ +class MultipleSelect extends AbstractSelect +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/RadioButtons.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/RadioButtons.php new file mode 100644 index 0000000000000..345aa289283e8 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/RadioButtons.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractSelect; + +/** + * Data provider for custom options from select group with type "Radio Buttons". + */ +class RadioButtons extends AbstractSelect +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_RADIO; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Time.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Time.php new file mode 100644 index 0000000000000..cc6fb1e86a6c6 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/Time.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; + +/** + * Data provider for custom options from date group with type "time". + */ +class Time extends AbstractBase +{ + /** + * @inheritdoc + */ + protected function getType(): string + { + return ProductCustomOptionInterface::OPTION_TYPE_TIME; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/ProductLayoutUpdateManager.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/ProductLayoutUpdateManager.php new file mode 100644 index 0000000000000..6c8826583be70 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/ProductLayoutUpdateManager.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager; + +/** + * Easy way to fake available files. + */ +class ProductLayoutUpdateManager extends LayoutUpdateManager +{ + /** + * @var array Keys are product IDs, values - file names. + */ + private $fakeFiles = []; + + /** + * Supply fake files for a product. + * + * @param int $forProductId + * @param string[]|null $files Pass null to reset. + */ + public function setFakeFiles(int $forProductId, ?array $files): void + { + if ($files === null) { + unset($this->fakeFiles[$forProductId]); + } else { + $this->fakeFiles[$forProductId] = $files; + } + } + + /** + * @inheritDoc + */ + public function fetchAvailableFiles(ProductInterface $product): array + { + if (array_key_exists($product->getId(), $this->fakeFiles)) { + return $this->fakeFiles[$product->getId()]; + } + + return parent::fetchAvailableFiles($product); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Cms/Model/CustomLayoutManager.php b/dev/tests/integration/framework/Magento/TestFramework/Cms/Model/CustomLayoutManager.php new file mode 100644 index 0000000000000..527454c297d48 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Cms/Model/CustomLayoutManager.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Cms\Model; + +use Magento\Cms\Api\Data\PageInterface; + +/** + * Manager allowing to fake available files. + */ +class CustomLayoutManager extends \Magento\Cms\Model\Page\CustomLayout\CustomLayoutManager +{ + /** + * @var string[][] + */ + private $files = []; + + /** + * Fake available files for given page. + * + * Pass null to unassign fake files. + * + * @param int $forPageId + * @param string[]|null $files + * @return void + */ + public function fakeAvailableFiles(int $forPageId, ?array $files): void + { + if ($files === null) { + unset($this->files[$forPageId]); + } else { + $this->files[$forPageId] = $files; + } + } + + /** + * @inheritDoc + */ + public function fetchAvailableFiles(PageInterface $page): array + { + if (array_key_exists($page->getId(), $this->files)) { + return $this->files[$page->getId()]; + } + + return parent::fetchAvailableFiles($page); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/AbstractAttributeDataWithOptions.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/AbstractAttributeDataWithOptions.php new file mode 100644 index 0000000000000..8f25651e2e036 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/AbstractAttributeDataWithOptions.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Base POST data for create attribute with options. + */ +abstract class AbstractAttributeDataWithOptions extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['serialized_options_arr'] = $this->getOptionsDataArr(); + $this->defaultAttributePostData['is_filterable'] = '0'; + $this->defaultAttributePostData['is_filterable_in_search'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + $result = parent::getAttributeData(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithErrorMessage(): array + { + $wrongSerializeMessage = 'The attribute couldn\'t be saved due to an error. Verify your information and '; + $wrongSerializeMessage .= 'try again. If the error persists, please try again later.'; + + return array_replace_recursive( + parent::getAttributeDataWithErrorMessage(), + [ + "{$this->getFrontendInput()}_with_wrong_serialized_options" => [ + array_merge( + $this->defaultAttributePostData, + [ + 'serialized_options_arr' => [], + 'serialized_options' => '?.\\//', + ] + ), + (string)__($wrongSerializeMessage) + ], + ] + ); + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + $result = parent::getAttributeDataWithCheckArray(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * Return attribute options data. + * + * @return array + */ + protected function getOptionsDataArr(): array + { + return [ + [ + 'option' => [ + 'order' => [ + 'option_0' => '1', + ], + 'value' => [ + 'option_0' => [ + 'Admin value 1', + 'Default store view value 1', + ], + ], + 'delete' => [ + 'option_0' => '', + ], + ], + ], + [ + 'option' => [ + 'order' => [ + 'option_1' => '2', + ], + 'value' => [ + 'option_1' => [ + 'Admin value 2', + 'Default store view value 2', + ], + ], + 'delete' => [ + 'option_1' => '', + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/AbstractBaseAttributeData.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/AbstractBaseAttributeData.php new file mode 100644 index 0000000000000..af9e58d02fb5a --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/AbstractBaseAttributeData.php @@ -0,0 +1,224 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +use Magento\Store\Model\Store; + +/** + * Base POST data for create attribute. + */ +abstract class AbstractBaseAttributeData +{ + /** + * Default POST data for create attribute. + * + * @var array + */ + protected $defaultAttributePostData = [ + 'active_tab' => 'main', + 'frontend_label' => [ + Store::DEFAULT_STORE_ID => 'Test attribute name', + ], + 'is_required' => '0', + 'dropdown_attribute_validation' => '', + 'dropdown_attribute_validation_unique' => '', + 'attribute_code' => '', + 'is_global' => '0', + 'default_value_text' => '', + 'default_value_yesno' => '0', + 'default_value_date' => '', + 'default_value_textarea' => '', + 'is_unique' => '0', + 'is_used_in_grid' => '1', + 'is_visible_in_grid' => '1', + 'is_filterable_in_grid' => '1', + 'is_searchable' => '0', + 'is_comparable' => '0', + 'is_used_for_promo_rules' => '0', + 'is_html_allowed_on_front' => '1', + 'is_visible_on_front' => '0', + 'used_in_product_listing' => '0', + ]; + + /** + * @inheritdoc + */ + public function __construct() + { + $this->defaultAttributePostData['frontend_input'] = $this->getFrontendInput(); + } + + /** + * Return create product attribute data set. + * + * @return array + */ + public function getAttributeData(): array + { + return [ + "{$this->getFrontendInput()}_with_required_fields" => [ + $this->defaultAttributePostData, + ], + "{$this->getFrontendInput()}_with_store_view_scope" => [ + $this->defaultAttributePostData, + ], + "{$this->getFrontendInput()}_with_global_scope" => [ + array_merge($this->defaultAttributePostData, ['is_global' => '1']), + ], + "{$this->getFrontendInput()}_with_website_scope" => [ + array_merge($this->defaultAttributePostData, ['is_global' => '2']), + ], + "{$this->getFrontendInput()}_with_attribute_code" => [ + array_merge($this->defaultAttributePostData, ['attribute_code' => 'test_custom_attribute_code']), + ], + "{$this->getFrontendInput()}_with_default_value" => [ + array_merge($this->defaultAttributePostData, ['default_value_text' => 'Default attribute value']), + ], + "{$this->getFrontendInput()}_without_default_value" => [ + $this->defaultAttributePostData, + ], + "{$this->getFrontendInput()}_with_unique_value" => [ + array_merge($this->defaultAttributePostData, ['is_unique' => '1']), + ], + "{$this->getFrontendInput()}_without_unique_value" => [ + $this->defaultAttributePostData, + ], + "{$this->getFrontendInput()}_with_enabled_add_to_column_options" => [ + array_merge($this->defaultAttributePostData, ['is_used_in_grid' => '1']), + ], + "{$this->getFrontendInput()}_without_enabled_add_to_column_options" => [ + array_merge($this->defaultAttributePostData, ['is_used_in_grid' => '0']), + ], + "{$this->getFrontendInput()}_with_enabled_use_in_filter_options" => [ + $this->defaultAttributePostData, + ], + "{$this->getFrontendInput()}_without_enabled_use_in_filter_options" => [ + array_merge($this->defaultAttributePostData, ['is_filterable_in_grid' => '0']), + ], + ]; + } + + /** + * Return create product attribute data set with error message. + * + * @return array + */ + public function getAttributeDataWithErrorMessage(): array + { + $wrongAttributeCode = 'Attribute code "????" is invalid. Please use only letters (a-z or A-Z), numbers '; + $wrongAttributeCode .= '(0-9) or underscore (_) in this field, and the first character should be a letter.'; + + return [ + "{$this->getFrontendInput()}_with_wrong_frontend_input" => [ + array_merge($this->defaultAttributePostData, ['frontend_input' => 'wrong_input_type']), + (string)__('Input type "wrong_input_type" not found in the input types list.') + ], + "{$this->getFrontendInput()}_with_wrong_attribute_code" => [ + array_merge($this->defaultAttributePostData, ['attribute_code' => '????']), + (string)__($wrongAttributeCode) + ], + ]; + } + + /** + * Return create product attribute data set with array for check data. + * + * @return array + */ + public function getAttributeDataWithCheckArray(): array + { + return array_merge_recursive( + $this->getAttributeData(), + [ + "{$this->getFrontendInput()}_with_required_fields" => [ + [ + 'attribute_code' => 'test_attribute_name', + ], + ], + "{$this->getFrontendInput()}_with_store_view_scope" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_global' => '0', + ], + ], + "{$this->getFrontendInput()}_with_global_scope" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_global' => '1', + ], + ], + "{$this->getFrontendInput()}_with_website_scope" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_global' => '2', + ], + ], + "{$this->getFrontendInput()}_with_attribute_code" => [ + [ + 'attribute_code' => 'test_custom_attribute_code', + ], + ], + "{$this->getFrontendInput()}_with_default_value" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'default_value' => 'Default attribute value', + ], + ], + "{$this->getFrontendInput()}_without_default_value" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'default_value_text' => '', + ], + ], + "{$this->getFrontendInput()}_with_unique_value" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_unique' => '1', + ], + ], + "{$this->getFrontendInput()}_without_unique_value" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_unique' => '0', + ], + ], + "{$this->getFrontendInput()}_with_enabled_add_to_column_options" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_used_in_grid' => '1', + ], + ], + "{$this->getFrontendInput()}_without_enabled_add_to_column_options" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_used_in_grid' => false, + ], + ], + "{$this->getFrontendInput()}_with_enabled_use_in_filter_options" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_filterable_in_grid' => '1', + ], + ], + "{$this->getFrontendInput()}_without_enabled_use_in_filter_options" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'is_filterable_in_grid' => false, + ], + ], + ] + ); + } + + /** + * Return attribute frontend input. + * + * @return string + */ + abstract protected function getFrontendInput(): string; +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/Date.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/Date.php new file mode 100644 index 0000000000000..7a6f8ee41c1f8 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/Date.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with input type date. + */ +class Date extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + return array_replace_recursive( + parent::getAttributeData(), + [ + "{$this->getFrontendInput()}_with_default_value" => [ + [ + 'default_value_text' => '', + 'default_value_date' => '10/29/2019', + ] + ] + ] + ); + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + return array_replace_recursive( + parent::getAttributeDataWithCheckArray(), + [ + "{$this->getFrontendInput()}_with_default_value" => [ + 1 => [ + 'default_value' => '2019-10-29 00:00:00', + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'date'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/DropDown.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/DropDown.php new file mode 100644 index 0000000000000..3c1acb5a33a54 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/DropDown.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with input type dropdown. + */ +class DropDown extends AbstractAttributeDataWithOptions +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + $this->defaultAttributePostData['swatch_input_type'] = 'dropdown'; + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'select'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/MultipleSelect.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/MultipleSelect.php new file mode 100644 index 0000000000000..5fb5f745aebdc --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/MultipleSelect.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with input type multiple select. + */ +class MultipleSelect extends AbstractAttributeDataWithOptions +{ + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'multiselect'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/Text.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/Text.php new file mode 100644 index 0000000000000..5b37248e27361 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/Text.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with input type text. + */ +class Text extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['frontend_class'] = ''; + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + return array_replace_recursive( + parent::getAttributeData(), + [ + "{$this->getFrontendInput()}_with_input_validation" => [ + array_merge($this->defaultAttributePostData, ['frontend_class' => 'validate-alpha']), + ], + "{$this->getFrontendInput()}_without_input_validation" => [ + $this->defaultAttributePostData, + ], + ] + ); + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + return array_merge_recursive( + parent::getAttributeDataWithCheckArray(), + [ + "{$this->getFrontendInput()}_with_input_validation" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'frontend_class' => 'validate-alpha', + ], + ], + "{$this->getFrontendInput()}_without_input_validation" => [ + [ + 'attribute_code' => 'test_attribute_name', + 'frontend_class' => '', + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'text'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/TextArea.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/TextArea.php new file mode 100644 index 0000000000000..7588b12700272 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/TextArea.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with text area input type. + */ +class TextArea extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + return array_replace_recursive( + parent::getAttributeData(), + [ + "{$this->getFrontendInput()}_with_default_value" => [ + [ + 'default_value_text' => '', + 'default_value_textarea' => 'Default attribute value', + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'textarea'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/TextEditor.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/TextEditor.php new file mode 100644 index 0000000000000..d7a6276c1720f --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/TextEditor.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with text editor input type. + */ +class TextEditor extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + return array_replace_recursive( + parent::getAttributeData(), + [ + "{$this->getFrontendInput()}_with_default_value" => [ + [ + 'default_value_text' => '', + 'default_value_textarea' => 'Default attribute value', + ], + ], + ] + ); + } + + /** + * @inheritDoc + */ + public function getAttributeDataWithCheckArray(): array + { + return array_replace_recursive( + parent::getAttributeDataWithCheckArray(), + [ + "{$this->getFrontendInput()}_with_required_fields" => [ + 1 => [ + 'frontend_input' => 'textarea', + ], + ], + "{$this->getFrontendInput()}_with_store_view_scope" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_global_scope" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_website_scope" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_attribute_code" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_default_value" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_without_default_value" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_unique_value" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_without_unique_value" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_enabled_add_to_column_options" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_without_enabled_add_to_column_options" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_with_enabled_use_in_filter_options" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + "{$this->getFrontendInput()}_without_enabled_use_in_filter_options" => [ + 1 => [ + 'frontend_input' => 'textarea' + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'texteditor'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/YesNo.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/YesNo.php new file mode 100644 index 0000000000000..8fece70f0273c --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/Attribute/DataProvider/YesNo.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\Attribute\DataProvider; + +/** + * Product attribute data for attribute with yes/no input type. + */ +class YesNo extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['is_filterable'] = '0'; + $this->defaultAttributePostData['is_filterable_in_search'] = '0'; + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + return array_replace_recursive( + parent::getAttributeData(), + [ + "{$this->getFrontendInput()}_with_default_value" => [ + [ + 'default_value_text' => '', + 'default_value_yesno' => 1, + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + return array_replace_recursive( + parent::getAttributeDataWithCheckArray(), + [ + "{$this->getFrontendInput()}_with_default_value" => [ + 1 => [ + 'default_value' => 1, + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'boolean'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/GetAttributeGroupByName.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/GetAttributeGroupByName.php new file mode 100644 index 0000000000000..65ebe326fb939 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/GetAttributeGroupByName.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model; + +use Magento\Eav\Api\AttributeGroupRepositoryInterface; +use Magento\Eav\Api\Data\AttributeGroupInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Search and return attribute group by name. + */ +class GetAttributeGroupByName +{ + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var AttributeGroupRepositoryInterface + */ + private $groupRepository; + + /** + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param AttributeGroupRepositoryInterface $attributeGroupRepository + */ + public function __construct( + SearchCriteriaBuilder $searchCriteriaBuilder, + AttributeGroupRepositoryInterface $attributeGroupRepository + ) { + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->groupRepository = $attributeGroupRepository; + } + + /** + * Returns attribute group by name. + * + * @param int $setId + * @param string $groupName + * @return AttributeGroupInterface|null + */ + public function execute(int $setId, string $groupName): ?AttributeGroupInterface + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter( + AttributeGroupInterface::GROUP_NAME, + $groupName + )->addFilter( + AttributeGroupInterface::ATTRIBUTE_SET_ID, + $setId + )->create(); + $result = $this->groupRepository->getList($searchCriteria)->getItems(); + + return array_shift($result); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/GetAttributeSetByName.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/GetAttributeSetByName.php new file mode 100644 index 0000000000000..d7a7f0646742a --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/GetAttributeSetByName.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model; + +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Eav\Api\Data\AttributeSetInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Search and return attribute set by name. + */ +class GetAttributeSetByName +{ + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var AttributeSetRepositoryInterface + */ + private $attributeSetRepository; + + /** + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param AttributeSetRepositoryInterface $attributeSetRepository + */ + public function __construct( + SearchCriteriaBuilder $searchCriteriaBuilder, + AttributeSetRepositoryInterface $attributeSetRepository + ) { + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->attributeSetRepository = $attributeSetRepository; + } + + /** + * Find attribute set by name and return it. + * + * @param string $attributeSetName + * @return AttributeSetInterface|null + */ + public function execute(string $attributeSetName): ?AttributeSetInterface + { + $this->searchCriteriaBuilder->addFilter('attribute_set_name', $attributeSetName); + $searchCriteria = $this->searchCriteriaBuilder->create(); + $result = $this->attributeSetRepository->getList($searchCriteria); + $items = $result->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/ResourceModel/GetEntityIdByAttributeId.php b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/ResourceModel/GetEntityIdByAttributeId.php new file mode 100644 index 0000000000000..edb75a5d8d1bd --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Eav/Model/ResourceModel/GetEntityIdByAttributeId.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Eav\Model\ResourceModel; + +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set as AttributeSetResource; + +/** + * Search and return attribute data from eav entity attribute table. + */ +class GetEntityIdByAttributeId +{ + /** + * @var AttributeSetResource + */ + private $attributeSetResource; + + /** + * @param AttributeSetResource $setResource + */ + public function __construct( + AttributeSetResource $setResource + ) { + $this->attributeSetResource = $setResource; + } + + /** + * Returns entity attribute by id. + * + * @param int $setId + * @param int $attributeId + * @return int|null + */ + public function execute(int $setId, int $attributeId): ?int + { + $select = $this->attributeSetResource->getConnection()->select() + ->from($this->attributeSetResource->getTable('eav_entity_attribute')) + ->where('attribute_set_id = ?', $setId) + ->where('attribute_id = ?', $attributeId); + + $result = $this->attributeSetResource->getConnection()->fetchOne($select); + return $result ? (int)$result : null; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Helper/Memory.php b/dev/tests/integration/framework/Magento/TestFramework/Helper/Memory.php index 0924bf8b7db0b..933d86fe6a4f5 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Helper/Memory.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Helper/Memory.php @@ -10,6 +10,9 @@ */ namespace Magento\TestFramework\Helper; +/** + * Integration Test Framework memory management logic. + */ class Memory { /** @@ -38,21 +41,21 @@ public function __construct(\Magento\Framework\Shell $shell) /** * Retrieve the effective memory usage of the current process * - * memory_get_usage() cannot be used because of the bug - * @link https://bugs.php.net/bug.php?id=62467 + * Function memory_get_usage() cannot be used because of the bug * + * @link https://bugs.php.net/bug.php?id=62467 * @return int Memory usage in bytes */ public function getRealMemoryUsage() { $pid = getmypid(); try { + // fall back to the Unix command line + $result = $this->_getUnixProcessMemoryUsage($pid); + } catch (\Magento\Framework\Exception\LocalizedException $e) { // try to use the Windows command line // some ports of Unix commands on Windows, such as MinGW, have limited capabilities and cannot be used $result = $this->_getWinProcessMemoryUsage($pid); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - // fall back to the Unix command line - $result = $this->_getUnixProcessMemoryUsage($pid); } return $result; } @@ -100,9 +103,11 @@ protected function _getWinProcessMemoryUsage($pid) * @return int * @throws \InvalidArgumentException * @throws \OutOfBoundsException + * phpcs:disable Magento2.Functions.StaticFunction */ public static function convertToBytes($number) { + // phpcs:enable Magento2.Functions.StaticFunction if (!preg_match('/^(.*\d)\h*(\D)$/', $number, $matches)) { throw new \InvalidArgumentException("Number format '{$number}' is not recognized."); } @@ -132,12 +137,14 @@ public static function convertToBytes($number) * - but the value has only one delimiter, such as "234,56", then it is impossible to know whether it is decimal * separator or not. Only knowing the right format would allow this. * - * @param $number + * @param string $number * @return string * @throws \InvalidArgumentException + * phpcs:disable Magento2.Functions.StaticFunction */ protected static function _convertToNumber($number) { + // phpcs:enable Magento2.Functions.StaticFunction preg_match_all('/(\D+)/', $number, $matches); if (count(array_unique($matches[0])) > 1) { throw new \InvalidArgumentException( @@ -152,9 +159,11 @@ protected static function _convertToNumber($number) * * @link http://php.net/manual/en/function.php-uname.php * @return boolean + * phpcs:disable Magento2.Functions.StaticFunction */ public static function isMacOs() { + // phpcs:enable Magento2.Functions.StaticFunction return strtoupper(PHP_OS) === 'DARWIN'; } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php index 9f697a1be6339..cd9512c227893 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php @@ -6,6 +6,9 @@ namespace Magento\TestFramework\Mail\Template; +/** + * Class TransportBuilderMock + */ class TransportBuilderMock extends \Magento\Framework\Mail\Template\TransportBuilder { /** @@ -38,11 +41,12 @@ public function getSentMessage() * Return transport mock. * * @return \Magento\TestFramework\Mail\TransportInterfaceMock + * @throws \Magento\Framework\Exception\LocalizedException */ public function getTransport() { $this->prepareMessage(); $this->reset(); - return new \Magento\TestFramework\Mail\TransportInterfaceMock(); + return new \Magento\TestFramework\Mail\TransportInterfaceMock($this->message); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php index 8f967b501a59f..5bf98b76e7d59 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php @@ -6,8 +6,28 @@ namespace Magento\TestFramework\Mail; +use Magento\Framework\Mail\EmailMessageInterface; + +/** + * Class TransportInterfaceMock + */ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterface { + /** + * @var null|EmailMessageInterface + */ + private $message; + + /** + * TransportInterfaceMock constructor. + * + * @param null|EmailMessageInterface $message + */ + public function __construct($message = null) + { + $this->message = $message; + } + /** * Mock of send a mail using transport * @@ -15,16 +35,17 @@ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterfa */ public function sendMessage() { + //phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired return; } /** * Get message * - * @return string + * @return null|EmailMessageInterface */ public function getMessage() { - return ''; + return $this->message; } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Quote/Model/GetQuoteByReservedOrderId.php b/dev/tests/integration/framework/Magento/TestFramework/Quote/Model/GetQuoteByReservedOrderId.php new file mode 100644 index 0000000000000..bd83bc595692f --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Quote/Model/GetQuoteByReservedOrderId.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Quote\Model; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Search and return quote by reserved order id. + */ +class GetQuoteByReservedOrderId +{ + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var CartRepositoryInterface */ + private $cartRepository; + + /** + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param CartRepositoryInterface $cartRepository + */ + public function __construct(SearchCriteriaBuilder $searchCriteriaBuilder, CartRepositoryInterface $cartRepository) + { + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->cartRepository = $cartRepository; + } + + /** + * Return quote by reserved order id. + * + * @param string $reservedOrderId + * @return CartInterface|null + */ + public function execute(string $reservedOrderId): ?CartInterface + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId)->create(); + $quotes = $this->cartRepository->getList($searchCriteria)->getItems(); + + return array_shift($quotes); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/SalesRule/Model/GetSalesRuleByName.php b/dev/tests/integration/framework/Magento/TestFramework/SalesRule/Model/GetSalesRuleByName.php new file mode 100644 index 0000000000000..db94d9ee97e04 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/SalesRule/Model/GetSalesRuleByName.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\SalesRule\Model; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Api\Data\RuleInterface; + +/** + * Search and return Sales rule by name. + */ +class GetSalesRuleByName +{ + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var RuleRepositoryInterface */ + private $ruleRepository; + + /** + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RuleRepositoryInterface $ruleRepository + */ + public function __construct(SearchCriteriaBuilder $searchCriteriaBuilder, RuleRepositoryInterface $ruleRepository) + { + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->ruleRepository = $ruleRepository; + } + + /** + * Return Sales Rule by name. + * + * @param string $name + * @return RuleInterface|null + */ + public function execute(string $name): ?RuleInterface + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter('name', $name)->create(); + $salesRules = $this->ruleRepository->getList($searchCriteria)->getItems(); + + return array_shift($salesRules); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/AbstractSwatchAttributeData.php b/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/AbstractSwatchAttributeData.php new file mode 100644 index 0000000000000..ce9a6b551986c --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/AbstractSwatchAttributeData.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Swatches\Model\Attribute\DataProvider; + +use Magento\TestFramework\Eav\Model\Attribute\DataProvider\AbstractAttributeDataWithOptions; + +/** + * Base attribute data for swatch attributes. + */ +abstract class AbstractSwatchAttributeData extends AbstractAttributeDataWithOptions +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData = array_replace( + $this->defaultAttributePostData, + [ + 'update_product_preview_image' => 0, + 'use_product_image_for_swatch' => 0, + 'visual_swatch_validation' => '', + 'visual_swatch_validation_unique' => '', + 'text_swatch_validation' => '', + 'text_swatch_validation_unique' => '', + 'used_for_sort_by' => 0, + ] + ); + $this->defaultAttributePostData['swatch_input_type'] = 'text'; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/TextSwatch.php b/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/TextSwatch.php new file mode 100644 index 0000000000000..c63873469e2f8 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/TextSwatch.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Swatches\Model\Attribute\DataProvider; + +use Magento\Swatches\Model\Swatch; + +/** + * Product attribute data for attribute with input type visual swatch. + */ +class TextSwatch extends AbstractSwatchAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['swatch_input_type'] = 'text'; + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + return array_replace_recursive( + parent::getAttributeDataWithCheckArray(), + [ + "{$this->getFrontendInput()}_with_required_fields" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_store_view_scope" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_global_scope" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_website_scope" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_attribute_code" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_unique_value" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_without_unique_value" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_enabled_add_to_column_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_without_enabled_add_to_column_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_enabled_use_in_filter_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_without_enabled_use_in_filter_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getOptionsDataArr(): array + { + return [ + [ + 'optiontext' => [ + 'order' => [ + 'option_0' => '1', + ], + 'value' => [ + 'option_0' => [ + 0 => 'Admin value description 1', + 1 => 'Default store view value description 1', + ], + ], + 'delete' => [ + 'option_0' => '', + ], + ], + 'defaulttext' => [ + 0 => 'option_0', + ], + 'swatchtext' => [ + 'value' => [ + 'option_0' => [ + 0 => 'Admin value 1', + 1 => 'Default store view value 1', + ], + ], + ], + ], + [ + 'optiontext' => [ + 'order' => [ + 'option_1' => '2', + ], + 'value' => [ + 'option_1' => [ + 0 => 'Admin value description 2', + 1 => 'Default store view value description 2', + ], + ], + 'delete' => [ + 'option_1' => '', + ], + ], + 'swatchtext' => [ + 'value' => [ + 'option_1' => [ + 0 => 'Admin value 2', + 1 => 'Default store view value 2', + ], + ], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return Swatch::SWATCH_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/VisualSwatch.php b/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/VisualSwatch.php new file mode 100644 index 0000000000000..b5e32c40ef8a1 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Swatches/Model/Attribute/DataProvider/VisualSwatch.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Swatches\Model\Attribute\DataProvider; + +use Magento\Swatches\Model\Swatch; + +/** + * Product attribute data for attribute with input type visual swatch. + */ +class VisualSwatch extends AbstractSwatchAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['swatch_input_type'] = 'visual'; + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + return array_replace_recursive( + parent::getAttributeDataWithCheckArray(), + [ + "{$this->getFrontendInput()}_with_required_fields" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_store_view_scope" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_global_scope" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_website_scope" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_attribute_code" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_unique_value" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_without_unique_value" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_enabled_add_to_column_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_without_enabled_add_to_column_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_with_enabled_use_in_filter_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + "{$this->getFrontendInput()}_without_enabled_use_in_filter_options" => [ + 1 => [ + 'frontend_input' => 'select', + ], + ], + ] + ); + } + + /** + * @inheritdoc + */ + protected function getOptionsDataArr(): array + { + return [ + [ + 'optionvisual' => [ + 'order' => [ + 'option_0' => '1', + ], + 'value' => [ + 'option_0' => [ + 0 => 'Admin black test 1', + 1 => 'Default store view black test 1', + ], + ], + 'delete' => [ + 'option_0' => '', + ] + ], + 'swatchvisual' => [ + 'value' => [ + 'option_0' => '#000000', + ] + ] + ], + [ + 'optionvisual' => [ + 'order' => [ + 'option_1' => '2', + ], + 'value' => [ + 'option_1' => [ + 0 => 'Admin white test 2', + 1 => 'Default store view white test 2', + ], + ], + 'delete' => [ + 'option_1' => '', + ], + ], + 'swatchvisual' => [ + 'value' => [ + 'option_1' => '#ffffff', + ], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return Swatch::SWATCH_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index 7a387bd41eec2..920cde4b7df09 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -41,6 +41,13 @@ abstract class AbstractBackendController extends \Magento\TestFramework\TestCase */ protected $httpMethod; + /** + * Expected no access response + * + * @var int + */ + protected $expectedNoAccessResponseCode = 403; + /** * @inheritDoc * @@ -84,21 +91,6 @@ protected function tearDown() parent::tearDown(); } - /** - * Utilize backend session model by default - * - * @param \PHPUnit\Framework\Constraint\Constraint $constraint - * @param string|null $messageType - * @param string $messageManagerClass - */ - public function assertSessionMessages( - \PHPUnit\Framework\Constraint\Constraint $constraint, - $messageType = null, - $messageManagerClass = \Magento\Framework\Message\Manager::class - ) { - parent::assertSessionMessages($constraint, $messageType, $messageManagerClass); - } - /** * Test ACL configuration for action working. */ @@ -111,8 +103,8 @@ public function testAclHasAccess() $this->getRequest()->setMethod($this->httpMethod); } $this->dispatch($this->uri); - $this->assertNotSame(403, $this->getResponse()->getHttpResponseCode()); $this->assertNotSame(404, $this->getResponse()->getHttpResponseCode()); + $this->assertNotSame($this->expectedNoAccessResponseCode, $this->getResponse()->getHttpResponseCode()); } /** @@ -130,6 +122,6 @@ public function testAclNoAccess() ->getAcl() ->deny(null, $this->resource); $this->dispatch($this->uri); - $this->assertSame(403, $this->getResponse()->getHttpResponseCode()); + $this->assertSame($this->expectedNoAccessResponseCode, $this->getResponse()->getHttpResponseCode()); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Weee/Model/Attribute/DataProvider/FixedProductTax.php b/dev/tests/integration/framework/Magento/TestFramework/Weee/Model/Attribute/DataProvider/FixedProductTax.php new file mode 100644 index 0000000000000..2f1f625ad48ac --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Weee/Model/Attribute/DataProvider/FixedProductTax.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Weee\Model\Attribute\DataProvider; + +use Magento\TestFramework\Eav\Model\Attribute\DataProvider\AbstractBaseAttributeData; + +/** + * Product attribute data for attribute with input type fixed product tax. + */ +class FixedProductTax extends AbstractBaseAttributeData +{ + /** + * @inheritdoc + */ + public function __construct() + { + parent::__construct(); + $this->defaultAttributePostData['used_for_sort_by'] = '0'; + } + + /** + * @inheritdoc + */ + public function getAttributeData(): array + { + $result = parent::getAttributeData(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + public function getAttributeDataWithCheckArray(): array + { + $result = parent::getAttributeDataWithCheckArray(); + unset($result["{$this->getFrontendInput()}_with_default_value"]); + unset($result["{$this->getFrontendInput()}_without_default_value"]); + + return $result; + } + + /** + * @inheritdoc + */ + protected function getFrontendInput(): string + { + return 'weee'; + } +} diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Helper/MemoryTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Helper/MemoryTest.php index a033aba7e90b6..04a82607668ec 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Helper/MemoryTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Helper/MemoryTest.php @@ -21,16 +21,7 @@ public function testGetRealMemoryUsageUnix() { $object = new \Magento\TestFramework\Helper\Memory($this->_shell); $this->_shell->expects( - $this->at(0) - )->method( - 'execute' - )->with( - $this->stringStartsWith('tasklist.exe ') - )->will( - $this->throwException(new \Magento\Framework\Exception\LocalizedException(__('command not found'))) - ); - $this->_shell->expects( - $this->at(1) + $this->once() )->method( 'execute' )->with( @@ -44,7 +35,16 @@ public function testGetRealMemoryUsageUnix() public function testGetRealMemoryUsageWin() { $this->_shell->expects( - $this->once() + $this->at(0) + )->method( + 'execute' + )->with( + $this->stringStartsWith('ps ') + )->will( + $this->throwException(new \Magento\Framework\Exception\LocalizedException(__('command not found'))) + ); + $this->_shell->expects( + $this->at(1) )->method( 'execute' )->with( diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php index b4f38d207c1f4..ce0cc79b0a5e0 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php @@ -6,6 +6,7 @@ namespace Magento\AdvancedPricingImportExport\Model\Export; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Csv; use Magento\TestFramework\Indexer\TestCase; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Filesystem; @@ -19,6 +20,8 @@ /** * Advanced pricing test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AdvancedPricingTest extends TestCase { @@ -161,6 +164,110 @@ public function testExportMultipleWebsites() } } + /** + * Export and Import of Advanced Pricing with different Price Types. + * + * @magentoDataFixture Magento/Catalog/_files/two_simple_products_with_tier_price.php + * @return void + */ + public function testExportImportOfAdvancedPricing(): void + { + $csvfile = uniqid('importexport_') . '.csv'; + $exportContent = $this->exportData($csvfile); + $this->assertContains( + 'second_simple,"All Websites [USD]","ALL GROUPS",10.0000,3.00,Discount', + $exportContent + ); + $this->assertContains( + 'simple,"All Websites [USD]",General,5.0000,95.000000,Fixed', + $exportContent + ); + $this->updateTierPriceDataInCsv($csvfile); + $this->importData($csvfile); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $firstProductTierPrices = $productRepository->get('simple')->getTierPrices(); + $secondProductTierPrices = $productRepository->get('second_simple')->getTierPrices(); + + $this->assertSame( + ['0', '1'], + [ + $firstProductTierPrices[0]->getExtensionAttributes()->getWebsiteId(), + $firstProductTierPrices[0]->getCustomerGroupId(), + ] + ); + + $this->assertEquals( + ['5.0000', '90.000000'], + [ + $firstProductTierPrices[0]->getQty(), + $firstProductTierPrices[0]->getValue(), + ], + '', + 0.1 + ); + + $this->assertSame( + ['0', \Magento\Customer\Model\Group::CUST_GROUP_ALL], + [ + $secondProductTierPrices[0]->getExtensionAttributes()->getWebsiteId(), + $secondProductTierPrices[0]->getCustomerGroupId(), + ] + ); + + $this->assertEquals( + ['5.00', '10.0000'], + [ + $secondProductTierPrices[0]->getExtensionAttributes()->getPercentageValue(), + $secondProductTierPrices[0]->getQty(), + ], + '', + 0.1 + ); + } + + /** + * Update tier price data in CSV. + * + * @param string $csvfile + * @return void + */ + private function updateTierPriceDataInCsv(string $csvfile): void + { + $csvNewData = [ + 0 => [ + 0 => 'sku', + 1 => 'tier_price_website', + 2 => 'tier_price_customer_group', + 3 => 'tier_price_qty', + 4 => 'tier_price', + 5 => 'tier_price_value_type', + ], + 1 => [ + 0 => 'simple', + 1 => 'All Websites [USD]', + 2 => 'General', + 3 => '5', + 4 => '90', + 5 => 'Fixed', + ], + 2 => [ + 0 => 'second_simple', + 1 => 'All Websites [USD]', + 2 => 'ALL GROUPS', + 3 => '10', + 4 => '5', + 5 => 'Discount', + ], + ]; + + /** @var Csv $csv */ + $csv = $this->objectManager->get(Csv::class); + $varDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $csv->appendData($varDirectory->getAbsolutePath($csvfile), $csvNewData); + } + /** * @param string $csvFile * @return string diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php index e74f995f8b57b..7e0d7594c3510 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php @@ -52,7 +52,22 @@ 'error_code' => 2222, 'result_message' => 'Entity with ID=4 does not exist', ], - + [ + 'bulk_uuid' => 'bulk-uuid-searchable-6', + 'topic_name' => 'topic-5', + 'serialized_data' => json_encode(['entity_id' => 5]), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + 'error_code' => null, + 'result_message' => '', + ], + [ + 'bulk_uuid' => 'bulk-uuid-searchable-6', + 'topic_name' => 'topic-5', + 'serialized_data' => json_encode(['entity_id' => 5]), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + 'error_code' => null, + 'result_message' => '', + ] ]; $bulkQuery = "INSERT INTO {$bulkTable} (`uuid`, `user_id`, `description`, `operation_count`, `start_time`)" diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php index 794e589002e73..fa3869d49bd2a 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php @@ -108,7 +108,7 @@ public function testDispatchToPlaceOrderWithRegisteredCustomer(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -142,12 +142,12 @@ public function testDispatchToPlaceOrderWithRegisteredCustomer(): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php index 070543a0880e8..4946448f91ccc 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php @@ -108,7 +108,7 @@ public function testDispatchToPlaceAnOrderWithAuthorizenet(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -137,12 +137,12 @@ public function testDispatchToPlaceAnOrderWithAuthorizenet(): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Tab/Products/ViewedTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Tab/Products/ViewedTest.php index 0c400cd76b9a1..f1a9e97ecaa9c 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Tab/Products/ViewedTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Tab/Products/ViewedTest.php @@ -49,6 +49,7 @@ protected function setUp() * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoConfigFixture default/reports/options/enabled 1 */ public function testGetPreparedCollectionProductPrice() { @@ -57,9 +58,11 @@ public function testGetPreparedCollectionProductPrice() $product = $this->productRepository->getById(1); $this->eventManager->dispatch('catalog_controller_product_view', ['product' => $product]); + $collection = $viewedProductsTabBlock->getPreparedCollection(); + $this->assertEquals( 10, - $viewedProductsTabBlock->getPreparedCollection()->getFirstItem()->getDataByKey('price') + $collection->getFirstItem()->getDataByKey('price') ); } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php index bd4dd0c8daf0c..4466434caef0e 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php @@ -15,6 +15,7 @@ class ProductsViewedTest extends \Magento\TestFramework\TestCase\AbstractBackend /** * @magentoAppArea adminhtml * @magentoDataFixture Magento/Reports/_files/viewed_products.php + * @magentoConfigFixture default/reports/options/enabled 1 */ public function testExecute() { diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php index 5ca2bf1f73175..42b1c10ee301e 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model\Auth; +use Magento\TestFramework\Bootstrap as TestHelper; +use Magento\TestFramework\Helper\Bootstrap; + /** * @magentoAppArea adminhtml * @magentoAppIsolation enabled @@ -30,7 +34,7 @@ class SessionTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); @@ -52,8 +56,8 @@ public function testIsLoggedIn($loggedIn) { if ($loggedIn) { $this->auth->login( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestHelper::ADMIN_NAME, + TestHelper::ADMIN_PASSWORD ); } $this->assertEquals($loggedIn, $this->authSession->isLoggedIn()); diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php index d1252be2c4b53..2754e055b71fe 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php @@ -6,6 +6,7 @@ namespace Magento\Backend\Model\Locale; use Magento\Framework\Locale\Resolver; +use Magento\TestFramework\Helper\Bootstrap; /** * @magentoAppArea adminhtml @@ -20,7 +21,7 @@ class ResolverTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->_model = Bootstrap::getObjectManager()->create( \Magento\Backend\Model\Locale\Resolver::class ); } @@ -39,11 +40,11 @@ public function testSetLocaleWithDefaultLocale() public function testSetLocaleWithBaseInterfaceLocale() { $user = new \Magento\Framework\DataObject(); - $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $session = Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class ); $session->setUser($user); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class )->getUser()->setInterfaceLocale( 'fr_FR' @@ -56,7 +57,7 @@ public function testSetLocaleWithBaseInterfaceLocale() */ public function testSetLocaleWithSessionLocale() { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Session::class )->setSessionLocale( 'es_ES' @@ -69,12 +70,44 @@ public function testSetLocaleWithSessionLocale() */ public function testSetLocaleWithRequestLocale() { - $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $request = Bootstrap::getObjectManager() ->get(\Magento\Framework\App\RequestInterface::class); $request->setPostValue(['locale' => 'de_DE']); $this->_checkSetLocale('de_DE'); } + /** + * Tests setLocale() with parameter + * + * @param string|null $localeParam + * @param string|null $localeRequestParam + * @param string $localeExpected + * @dataProvider setLocaleWithParameterDataProvider + */ + public function testSetLocaleWithParameter( + ?string $localeParam, + ?string $localeRequestParam, + string $localeExpected + ) { + $request = Bootstrap::getObjectManager() + ->get(\Magento\Framework\App\RequestInterface::class); + $request->setPostValue(['locale' => $localeRequestParam]); + $this->_model->setLocale($localeParam); + $this->assertEquals($localeExpected, $this->_model->getLocale()); + } + + /** + * @return array + */ + public function setLocaleWithParameterDataProvider(): array + { + return [ + ['ko_KR', 'ja_JP', 'ja_JP'], + ['ko_KR', null, 'ko_KR'], + [null, 'ja_JP', 'ja_JP'], + ]; + } + /** * Check set locale * diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php index d6ea08a2f7ca3..55d8c6a6a2170 100644 --- a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php @@ -62,7 +62,7 @@ protected function tearDown() * during creation second partial invoice. * * @return void - * @magentoConfigFixture default_store payment/braintree/merchant_account_id Magneto + * @magentoConfigFixture default_store payment/braintree/merchant_account_id Magento * @magentoConfigFixture current_store payment/braintree/merchant_account_id USA_Merchant * @magentoDataFixture Magento/Braintree/Fixtures/partial_invoice.php */ @@ -71,11 +71,14 @@ public function testCreatePartialInvoiceWithNonDefaultMerchantAccount(): void $order = $this->getOrder('100000002'); $this->adapter->method('sale') - ->with(self::callback(function ($request) { - self::assertEquals('USA_Merchant', $request['merchantAccountId']); - return true; - })) - ->willReturn($this->getTransactionStub()); + ->with( + self::callback( + function ($request) { + self::assertEquals('USA_Merchant', $request['merchantAccountId']); + return true; + } + ) + )->willReturn($this->getTransactionStub()); $uri = 'backend/sales/order_invoice/save/order_id/' . $order->getEntityId(); $this->prepareRequest($uri); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php index 91cea7dc96602..56bf2c53f6728 100644 --- a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php @@ -30,7 +30,7 @@ // assign virtual product to the billing address $billingAddress = $quote->getBillingAddress(); -$virtualItem = $items[sizeof($items) - 1]; +$virtualItem = $items[count($items) - 1]; $billingAddress->setTotalQty(1); $billingAddress->addItem($virtualItem); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/ProductTest.php new file mode 100644 index 0000000000000..4c598c16c3c47 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/ProductTest.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Controller\Adminhtml; + +use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Provide tests for product admin controllers. + * @magentoAppArea adminhtml + */ +class ProductTest extends AbstractBackendController +{ + /** + * Test bundle product duplicate won't remove bundle options from original product. + * + * @magentoDataFixture Magento/Catalog/_files/products_new.php + * @return void + */ + public function testDuplicateProduct() + { + $params = $this->getRequestParamsForDuplicate(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams(['type' => Type::TYPE_BUNDLE]); + $this->getRequest()->setPostValue($params); + $this->dispatch('backend/catalog/product/save'); + $this->assertSessionMessages( + $this->equalTo( + [ + 'You saved the product.', + 'You duplicated the product.', + ] + ), + MessageInterface::TYPE_SUCCESS + ); + $this->assertOptions(); + } + + /** + * Get necessary request post params for creating and duplicating bundle product. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function getRequestParamsForDuplicate() + { + $product = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class)->get('simple'); + return [ + 'product' => + [ + 'attribute_set_id' => '4', + 'gift_message_available' => '0', + 'use_config_gift_message_available' => '1', + 'stock_data' => + [ + 'min_qty_allowed_in_shopping_cart' => + [ + [ + 'record_id' => '0', + 'customer_group_id' => '32000', + 'min_sale_qty' => '', + ], + ], + 'min_qty' => '0', + 'max_sale_qty' => '10000', + 'notify_stock_qty' => '1', + 'min_sale_qty' => '1', + 'qty_increments' => '1', + 'use_config_manage_stock' => '1', + 'manage_stock' => '1', + 'use_config_min_qty' => '1', + 'use_config_max_sale_qty' => '1', + 'use_config_backorders' => '1', + 'backorders' => '0', + 'use_config_notify_stock_qty' => '1', + 'use_config_enable_qty_inc' => '1', + 'enable_qty_increments' => '0', + 'use_config_qty_increments' => '1', + 'use_config_min_sale_qty' => '1', + 'is_qty_decimal' => '0', + 'is_decimal_divided' => '0', + ], + 'status' => '1', + 'affect_product_custom_options' => '1', + 'name' => 'b1', + 'price' => '', + 'weight' => '', + 'url_key' => '', + 'special_price' => '', + 'quantity_and_stock_status' => + [ + 'qty' => '', + 'is_in_stock' => '1', + ], + 'sku_type' => '0', + 'price_type' => '0', + 'weight_type' => '0', + 'website_ids' => + [ + 1 => '1', + ], + 'sku' => 'b1', + 'meta_title' => 'b1', + 'meta_keyword' => 'b1', + 'meta_description' => 'b1 ', + 'tax_class_id' => '2', + 'product_has_weight' => '1', + 'visibility' => '4', + 'country_of_manufacture' => '', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_layout' => '', + 'price_view' => '0', + 'shipment_type' => '0', + 'news_from_date' => '', + 'news_to_date' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'special_from_date' => '', + 'special_to_date' => '', + 'description' => '', + 'short_description' => '', + 'custom_layout_update' => '', + 'image' => '', + 'small_image' => '', + 'thumbnail' => '', + ], + 'bundle_options' => + [ + 'bundle_options' => + [ + [ + 'record_id' => '0', + 'type' => 'select', + 'required' => '1', + 'title' => 'test option title', + 'position' => '1', + 'option_id' => '', + 'delete' => '', + 'bundle_selections' => + [ + [ + 'product_id' => $product->getId(), + 'name' => $product->getName(), + 'sku' => $product->getSku(), + 'price' => $product->getPrice(), + 'delete' => '', + 'selection_can_change_qty' => '', + 'selection_id' => '', + 'selection_price_type' => '0', + 'selection_price_value' => '', + 'selection_qty' => '1', + 'position' => '1', + 'option_id' => '', + 'record_id' => '1', + 'is_default' => '0', + ], + ], + 'bundle_button_proxy' => + [ + [ + 'entity_id' => '1', + ], + ], + ], + ], + ], + 'affect_bundle_product_selections' => '1', + 'back' => 'duplicate', + 'form_key' => Bootstrap::getObjectManager()->get(FormKey::class)->getFormKey(), + ]; + } + + /** + * Check options in created and duplicated products. + * + * @return void + */ + private function assertOptions() + { + $createdOptions = $this->getProductOptions('b1'); + $createdOption = array_shift($createdOptions); + $duplicatedOptions = $this->getProductOptions('b1-1'); + $duplicatedOption = array_shift($duplicatedOptions); + $this->assertNotEmpty($createdOption); + $this->assertNotEmpty($duplicatedOption); + $optionFields = ['type', 'title', 'position', 'required', 'default_title']; + foreach ($optionFields as $field) { + $this->assertSame($createdOption->getData($field), $duplicatedOption->getData($field)); + } + $createdLinks = $createdOption->getProductLinks(); + $createdLink = array_shift($createdLinks); + $duplicatedLinks = $duplicatedOption->getProductLinks(); + $duplicatedLink = array_shift($duplicatedLinks); + $this->assertNotEmpty($createdLink); + $this->assertNotEmpty($duplicatedLink); + $linkFields = [ + 'entity_id', + 'sku', + 'position', + 'is_default', + 'price', + 'qty', + 'selection_can_change_quantity', + 'price_type', + ]; + foreach ($linkFields as $field) { + $this->assertSame($createdLink->getData($field), $duplicatedLink->getData($field)); + } + } + + /** + * Get options for given product. + * + * @param string $sku + * @return OptionInterface[] + */ + private function getProductOptions(string $sku) + { + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = $product->getResource()->getIdBySku($sku); + $product->load($productId); + + return $product->getExtensionAttributes()->getBundleProductOptions(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php index 51250580eb6ae..77c1ade0fae3f 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php @@ -49,19 +49,20 @@ protected function setUp() /** * @magentoDataFixture Magento/Bundle/_files/product.php - * @covers \Magento\Indexer\Model\Indexer::reindexAll * @covers \Magento\Bundle\Model\Product\Type::getSearchableData * @magentoDbIsolation disabled */ - public function testPrepareProductIndexForBundleProduct() + public function testGetSearchableData() { - $this->indexer->reindexAll(); - - $select = $this->connectionMock->select()->from($this->resource->getTableName('catalogsearch_fulltext_scope1')) - ->where('`data_index` LIKE ?', '%' . 'Bundle Product Items' . '%'); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Model\Product $bundleProduct */ + $bundleProduct = $productRepository->get('bundle-product'); + $bundleType = $bundleProduct->getTypeInstance(); + /** @var \Magento\Bundle\Model\Product\Type $bundleType */ + $searchableData = $bundleType->getSearchableData($bundleProduct); - $result = $this->connectionMock->fetchAll($select); - $this->assertCount(1, $result); + $this->assertCount(1, $searchableData); + $this->assertEquals('Bundle Product Items', $searchableData[0]); } /** diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php new file mode 100644 index 0000000000000..c00fd2435a9f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/../../../Magento/Catalog/_files/multiple_products.php'; + +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = [10, 11, 12]; +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(3) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceView(0) + ->setSkuType(1) + ->setWeightType(1) + ->setPriceType(0) + ->setPrice(10.0) + ->setSpecialPrice(10) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // Required "Drop-down" option + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + 'delete' => '', + ], + + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_price_value' => 2.75, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 6.75, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ] + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $link->setPrice($linkData['selection_price_value']); + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +/** @var $tierPriceExtensionAttributesFactory */ +$tierPriceExtensionAttributesFactory = $objectManager->create(ProductTierPriceExtensionFactory::class); +$tierPriceExtensionAttribute = $tierPriceExtensionAttributesFactory->create()->setPercentageValue(10); +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttribute); +$product->setTierPrices($tierPrices); +$product->save(); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options_rollback.php new file mode 100644 index 0000000000000..a17e2604d9d01 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options_rollback.php @@ -0,0 +1,7 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/../../../Magento/Bundle/_files/product_with_multiple_options_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php index 03949115ea62c..c79e943ba4be3 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options.php @@ -8,7 +8,7 @@ use Magento\TestFramework\Helper\Bootstrap; -require __DIR__ . 'product_with_multiple_options.php'; +require __DIR__ . '/product_with_multiple_options.php'; $objectManager = Bootstrap::getObjectManager(); @@ -49,6 +49,14 @@ $cart->getQuote()->setReservedOrderId('test_cart_with_bundle_and_options'); $cart->save(); +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($cart->getQuote()->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); + /** @var $objectManager \Magento\TestFramework\ObjectManager */ $objectManager = Bootstrap::getObjectManager(); $objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php index d32d6fab33319..591aec9190f9f 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/quote_with_bundle_and_options_rollback.php @@ -22,7 +22,7 @@ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); -require __DIR__ . 'product_with_multiple_options_rollback.php'; +require __DIR__ . '/product_with_multiple_options_rollback.php'; $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TopMenuTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TopMenuTest.php new file mode 100644 index 0000000000000..0dc8016c3ef8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TopMenuTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Block\Html\Topmenu; +use PHPUnit\Framework\TestCase; + +/** + * Class checks top menu link behaviour. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class TopMenuTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Topmenu */ + private $block; + + /** @var CategoryFactory */ + private $categoryFactory; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->categoryFactory = $this->objectManager->create(CategoryFactory::class); + $this->categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Topmenu::class); + } + + /** + * Checks menu item displaying. + * + * @magentoDataFixture Magento/Catalog/_files/category.php + * @return void + */ + public function testTopMenuItemDisplay(): void + { + $output = $this->block->getHtml('level-top', 'submenu', 0); + $this->assertContains('Category 1', $output); + } + + /** + * Checks that menu item is not displayed if the category is disabled or include in menu is disabled. + * + * @dataProvider invisibilityDataProvider + * @param array $data + * @return void + */ + public function testTopMenuItemInvisibility(array $data): void + { + $category = $this->categoryFactory->create(); + $category->setData($data); + $this->categoryRepository->save($category); + $output = $this->block->getHtml('level-top', 'submenu', 0); + $this->assertEmpty($output, 'The category is displayed in top menu navigation'); + } + + /** + * @return array + */ + public function invisibilityDataProvider(): array + { + return [ + 'include_in_menu_disable' => [ + 'data' => [ + 'name' => 'Test Category', + 'path' => '1/2/', + 'is_active' => '1', + 'include_in_menu' => false, + ], + ], + 'category_disable' => [ + 'data' => [ + 'name' => 'Test Category 2', + 'path' => '1/2/', + 'is_active' => false, + 'include_in_menu' => true, + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/ProductInCategoriesViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/ProductInCategoriesViewTest.php new file mode 100644 index 0000000000000..b9adb051981c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/ProductInCategoriesViewTest.php @@ -0,0 +1,297 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\ListProduct; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Eav\Model\Entity\Collection\AbstractCollection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks products displaying on category page + * + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + */ +class ProductInCategoriesViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ListProduct */ + private $block; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(ListProduct::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_two_products.php + * @dataProvider productDataProvider + * @param array $data + * @return void + */ + public function testCategoryProductView(array $data): void + { + $this->updateProduct($data['sku'], $data); + $collection = $this->getCategoryProductCollection(333); + + $this->assertEquals(1, $collection->getSize()); + $this->assertEquals('simple333', $collection->getFirstItem()->getSku()); + } + + /** + * @return array + */ + public function productDataProvider(): array + { + return [ + 'simple_product_enabled_disabled' => [ + [ + 'sku' => 'simple2', + 'status' => 0, + ], + ], + 'simple_product_in_stock_out_of_stock' => [ + [ + 'sku' => 'simple2', + 'stock_data' => [ + 'use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0, + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @dataProvider productVisibilityProvider + * @param array $data + * @return void + */ + public function testCategoryProductVisibility(array $data): void + { + $this->updateProduct($data['data']['sku'], $data['data']); + $collection = $this->getCategoryProductCollection(333); + + $this->assertEquals($data['expected_count'], $collection->getSize()); + } + + /** + * @return array + */ + public function productVisibilityProvider(): array + { + return [ + 'not_visible' => [ + [ + 'data' => [ + 'sku' => 'simple333', + 'visibility' => Visibility::VISIBILITY_NOT_VISIBLE, + ], + 'expected_count' => 0, + ], + + ], + 'catalog_search' => [ + [ + 'data' => [ + 'sku' => 'simple333', + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + 'expected_count' => 1, + ], + + ], + 'search' => [ + [ + 'data' => [ + 'sku' => 'simple333', + 'visibility' => Visibility::VISIBILITY_IN_SEARCH, + ], + 'expected_count' => 0, + ], + ], + 'catalog' => [ + [ + 'data' => [ + 'sku' => 'simple333', + 'visibility' => Visibility::VISIBILITY_IN_CATALOG, + ], + 'expected_count' => 1, + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testAnchorCategoryProductVisibility(): void + { + $this->updateCategoryIsAnchor(400, true); + $this->assignProductCategories('simple2', [402]); + $parentCategoryCollection = $this->getCategoryProductCollection(400); + $childCategoryCollection = $this->getCategoryProductCollection(402, true); + + $this->assertEquals(1, $parentCategoryCollection->getSize()); + $this->assertEquals( + $childCategoryCollection->getAllIds(), + $parentCategoryCollection->getAllIds() + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testNonAnchorCategoryProductVisibility(): void + { + $this->updateCategoryIsAnchor(400, false); + $this->assignProductCategories('simple2', [402]); + $parentCategoryCollectionSize = $this->getCategoryProductCollection(400)->getSize(); + $childCategoryCollectionSize = $this->getCategoryProductCollection(402, true)->getSize(); + + $this->assertEquals(0, $parentCategoryCollectionSize); + $this->assertEquals(1, $childCategoryCollectionSize); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * @return void + */ + public function testCategoryProductViewOnMultiWebsite(): void + { + $this->assignProductCategories(['simple-1', 'simple-2'], [3, 333]); + $store = $this->storeManager->getStore('fixture_second_store'); + $currentStore = $this->storeManager->getStore(); + + try { + $this->storeManager->setCurrentStore($store->getId()); + $collection = $this->block->getLoadedProductCollection(); + $collectionSize = $collection->getSize(); + } finally { + $this->storeManager->setCurrentStore($currentStore); + } + + $this->assertEquals(1, $collectionSize); + $this->assertNull($collection->getItemByColumnValue('sku', 'simple-1')); + $this->assertNotNull($collection->getItemByColumnValue('sku', 'simple-2')); + } + + /** + * Set categories to the products + * + * @param string|array $sku + * @param $categoryIds + * @return void + */ + private function assignProductCategories($sku, array $categoryIds): void + { + $skus = !is_array($sku) ? [$sku] : $sku; + foreach ($skus as $sku) { + $product = $this->productRepository->get($sku); + $product->setCategoryIds($categoryIds); + $this->productRepository->save($product); + } + } + + /** + * Update product + * + * @param string $sku + * @param array $data + * @return void + */ + private function updateProduct(string $sku, array $data): void + { + $product = $this->productRepository->get($sku); + $product->addData($data); + $this->productRepository->save($product); + } + + /** + * Returns category collection by category id + * + * @param int $categoryId + * @param bool $refreshBlock + * @return AbstractCollection + */ + private function getCategoryProductCollection(int $categoryId, bool $refreshBlock = false): AbstractCollection + { + $block = $this->getListingBlock($refreshBlock); + $block->getLayer()->setCurrentCategory($categoryId); + + return $block->getLoadedProductCollection(); + } + + /** + * Update is_anchor attribute of the category + * + * @param int $categoryId + * @param bool $isAnchor + * @return void + */ + private function updateCategoryIsAnchor(int $categoryId, bool $isAnchor): void + { + $category = $this->categoryRepository->get($categoryId); + $category->setIsAnchor($isAnchor); + $this->categoryRepository->save($category); + } + + /** + * Get product listing block + * + * @param bool $refresh + * @return ListProduct + */ + private function getListingBlock(bool $refresh): ListProduct + { + if ($refresh) { + $this->block = $this->layout->createBlock(ListProduct::class); + } + + return $this->block; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php new file mode 100644 index 0000000000000..f8e778498211d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php @@ -0,0 +1,323 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\ProductList; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductLinkInterface; +use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Block\Product\AbstractProduct; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductLink\Link; +use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Class AbstractLinks - abstract class when testing blocks of linked products + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +abstract class AbstractLinksTest extends TestCase +{ + /** @var ObjectManager */ + protected $objectManager; + + /** @var ProductRepositoryInterface */ + protected $productRepository; + + /** @var LayoutInterface */ + protected $layout; + + /** @var ProductInterface|Product */ + protected $product; + + /** @var ProductLinkInterfaceFactory */ + protected $productLinkInterfaceFactory; + + /** @var StoreManagerInterface */ + protected $storeManager; + + /** @var array */ + protected $existingProducts = [ + 'wrong-simple' => [ + 'position' => 1, + ], + 'simple-249' => [ + 'position' => 2, + ], + 'simple-156' => [ + 'position' => 3, + ], + ]; + + /** @var AbstractProduct */ + protected $block; + + /** @var string */ + protected $linkType; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->productLinkInterfaceFactory = $this->objectManager->get(ProductLinkInterfaceFactory::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + } + + /** + * Provide test data to verify the display of linked products. + * + * @return array + */ + public function displayLinkedProductsProvider(): array + { + return [ + 'product_all_displayed' => [ + 'data' => [ + 'updateProducts' => [], + 'expectedProductLinks' => [ + 'wrong-simple', + 'simple-249', + 'simple-156', + ], + ], + ], + 'product_disabled' => [ + 'data' => [ + 'updateProducts' => [ + 'wrong-simple' => ['status' => Status::STATUS_DISABLED], + ], + 'expectedProductLinks' => [ + 'simple-249', + 'simple-156', + ], + ], + ], + 'product_invisibility' => [ + 'data' => [ + 'updateProducts' => [ + 'simple-249' => ['visibility' => Visibility::VISIBILITY_NOT_VISIBLE], + ], + 'expectedProductLinks' => [ + 'wrong-simple', + 'simple-156', + ], + ], + ], + 'product_invisible_in_catalog' => [ + 'data' => [ + 'updateProducts' => [ + 'simple-249' => ['visibility' => Visibility::VISIBILITY_IN_SEARCH], + ], + 'expectedProductLinks' => [ + 'wrong-simple', + 'simple-156', + ], + ], + ], + 'product_out_of_stock' => [ + 'data' => [ + 'updateProducts' => [ + 'simple-156' => [ + 'stock_data' => [ + 'use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0, + ], + ], + ], + 'expectedProductLinks' => [ + 'wrong-simple', + 'simple-249', + ], + ], + ], + ]; + } + + /** + * Provide test data to verify the display of linked products on different websites. + * + * @return array + */ + public function multipleWebsitesLinkedProductsProvider(): array + { + return [ + 'first_website' => [ + 'data' => [ + 'storeCode' => 'default', + 'productLinks' => [ + 'simple-2' => ['position' => 4], + ], + 'expectedProductLinks' => [ + 'wrong-simple', + 'simple-2', + ], + ], + ], + 'second_website' => [ + 'data' => [ + 'storeCode' => 'fixture_second_store', + 'productLinks' => [ + 'simple-2' => ['position' => 4], + ], + 'expectedProductLinks' => [ + 'simple-249', + 'simple-2', + ], + ], + ], + ]; + } + + /** + * Get test data to check position of related, up-sells and cross-sells products + * + * @return array + */ + protected function getPositionData(): array + { + return [ + 'productLinks' => array_replace_recursive( + $this->existingProducts, + [ + 'wrong-simple' => ['position' => 2], + 'simple-249' => ['position' => 3], + 'simple-156' => ['position' => 1], + ] + ), + 'expectedProductLinks' => [ + 'simple-156', + 'wrong-simple', + 'simple-249', + ], + ]; + } + + /** + * Prepare a block of linked products + * + * @return void + */ + protected function prepareBlock(): void + { + $this->block->setLayout($this->layout); + $this->block->setTemplate('Magento_Catalog::product/list/items.phtml'); + $this->block->setType($this->linkType); + } + + /** + * Set linked products by link type for current product received from array + * + * @param ProductInterface $product + * @param array $productData + * @return void + */ + private function setCustomProductLinks(ProductInterface $product, array $productData): void + { + $productLinks = []; + foreach ($productData as $sku => $data) { + /** @var ProductLinkInterface|Link $productLink */ + $productLink = $this->productLinkInterfaceFactory->create(); + $productLink->setSku($product->getSku()); + $productLink->setLinkedProductSku($sku); + if (isset($data['position'])) { + $productLink->setPosition($data['position']); + } + $productLink->setLinkType($this->linkType); + $productLinks[] = $productLink; + } + $product->setProductLinks($productLinks); + } + + /** + * Update product attributes + * + * @param array $products + * @return void + */ + protected function updateProducts(array $products): void + { + foreach ($products as $sku => $data) { + /** @var ProductInterface|Product $product */ + $product = $this->productRepository->get($sku); + $product->addData($data); + $this->productRepository->save($product); + } + } + + /** + * Get an array of received linked products + * + * @param array $items + * @return array + */ + protected function getActualLinks(array $items): array + { + $actualLinks = []; + /** @var ProductInterface $productItem */ + foreach ($items as $productItem) { + $actualLinks[] = $productItem->getSku(); + } + + return $actualLinks; + } + + /** + * Link products to an existing product + * + * @param string $sku + * @param array $productLinks + * @return void + */ + protected function linkProducts(string $sku, array $productLinks): void + { + $product = $this->productRepository->get($sku); + $this->setCustomProductLinks($product, $productLinks); + $this->productRepository->save($product); + } + + /** + * Prepare the necessary websites for all products + * + * @return array + */ + protected function prepareWebsiteIdsProducts(): array + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $defaultWebsiteId = $this->storeManager->getWebsite('base')->getId(); + + return [ + 'simple-1' => [ + 'website_ids' => [$defaultWebsiteId, $websiteId], + ], + 'simple-2' => [ + 'website_ids' => [$defaultWebsiteId, $websiteId], + ], + 'wrong-simple' => [ + 'website_ids' => [$defaultWebsiteId], + ], + 'simple-249' => [ + 'website_ids' => [$websiteId], + ], + 'simple-156' => [ + 'website_ids' => [], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php index 9f0f04508e468..c71a481a79379 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php @@ -3,84 +3,150 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Product\ProductList; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection as LinkProductCollection; + /** - * Test class for \Magento\Catalog\Block\Product\List\Related. + * Check the correct behavior of related products on the product view page * - * @magentoDataFixture Magento/Catalog/_files/products_related.php + * @see \Magento\Catalog\Block\Product\ProductList\Related * @magentoDbIsolation disabled + * @magentoAppArea frontend */ -class RelatedTest extends \PHPUnit\Framework\TestCase +class RelatedTest extends AbstractLinksTest { - /** - * @var \Magento\Catalog\Block\Product\ProductList\Related - */ + /** @var Related */ protected $block; /** - * @var \Magento\Catalog\Api\Data\ProductInterface - */ - protected $product; - - /** - * @var \Magento\Catalog\Api\Data\ProductInterface + * @inheritdoc */ - protected $relatedProduct; - protected function setUp() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - - $this->relatedProduct = $productRepository->get('simple'); - $this->product = $productRepository->get('simple_with_cross'); - $objectManager->get(\Magento\Framework\Registry::class)->register('product', $this->product); + parent::setUp(); - $this->block = $objectManager->get(\Magento\Framework\View\LayoutInterface::class) - ->createBlock(\Magento\Catalog\Block\Product\ProductList\Related::class); - - $this->block->setLayout($objectManager->get(\Magento\Framework\View\LayoutInterface::class)); - $this->block->setTemplate('Magento_Catalog::product/list/items.phtml'); - $this->block->setType('related'); - $this->block->addChild('addto', \Magento\Catalog\Block\Product\ProductList\Item\Container::class); - $this->block->getChildBlock( - 'addto' - )->addChild( - 'compare', - \Magento\Catalog\Block\Product\ProductList\Item\AddTo\Compare::class, - ['template' => 'Magento_Catalog::product/list/addto/compare.phtml'] - ); + $this->block = $this->layout->createBlock(Related::class); + $this->linkType = 'related'; } /** - * @magentoAppIsolation enabled + * Checks for a related product when block code is generated + * + * @magentoDataFixture Magento/Catalog/_files/products_related.php + * @return void */ - public function testAll() + public function testAll(): void { + /** @var ProductInterface $relatedProduct */ + $relatedProduct = $this->productRepository->get('simple'); + $this->product = $this->productRepository->get('simple_with_cross'); + $this->block->setProduct($this->product); + $this->prepareBlock(); $html = $this->block->toHtml(); $this->assertNotEmpty($html); - $this->assertContains('Simple Related Product', $html); + $this->assertContains($relatedProduct->getName(), $html); /* name */ - $this->assertContains('"product":"' . $this->relatedProduct->getId() . '"', $html); + $this->assertContains('id="related-checkbox' . $relatedProduct->getId() . '"', $html); /* part of url */ $this->assertInstanceOf( - \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection::class, + LinkProductCollection::class, $this->block->getItems() ); } /** - * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/products_related.php + * @return void */ - public function testGetIdentities() + public function testGetIdentities(): void { - $expectedTags = ['cat_p_' . $this->relatedProduct->getId(), 'cat_p']; + /** @var ProductInterface $relatedProduct */ + $relatedProduct = $this->productRepository->get('simple'); + $this->product = $this->productRepository->get('simple_with_cross'); + $this->block->setProduct($this->product); + $this->prepareBlock(); + $expectedTags = ['cat_p_' . $relatedProduct->getId(), 'cat_p']; $tags = $this->block->getIdentities(); $this->assertEquals($expectedTags, $tags); } + + /** + * Test the display of related products in the block + * + * @dataProvider displayLinkedProductsProvider + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @param array $data + * @return void + */ + public function testDisplayRelatedProducts(array $data): void + { + $this->updateProducts($data['updateProducts']); + $this->linkProducts('simple', $this->existingProducts); + $this->product = $this->productRepository->get('simple'); + $this->block->setProduct($this->product); + $items = $this->block->getItems()->getItems(); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected related products do not match actual related products!' + ); + } + + /** + * Test the position of related products in the block + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @return void + */ + public function testPositionRelatedProducts(): void + { + $data = $this->getPositionData(); + $this->linkProducts('simple', $data['productLinks']); + $this->product = $this->productRepository->get('simple'); + $this->block->setProduct($this->product); + $items = $this->block->getItems()->getItems(); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected related products do not match actual related products!' + ); + } + + /** + * Test the display of related products in the block on different websites + * + * @dataProvider multipleWebsitesLinkedProductsProvider + * @magentoDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @magentoAppIsolation enabled + * @param array $data + * @return void + */ + public function testMultipleWebsitesRelatedProducts(array $data): void + { + $this->updateProducts($this->prepareWebsiteIdsProducts()); + $productLinks = array_replace_recursive($this->existingProducts, $data['productLinks']); + $this->linkProducts('simple-1', $productLinks); + $this->product = $this->productRepository->get( + 'simple-1', + false, + $this->storeManager->getStore($data['storeCode'])->getId() + ); + $this->block->setProduct($this->product); + $items = $this->block->getItems()->getItems(); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected related products do not match actual related products!' + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php index 57ccb48b8df69..f989393d5da63 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php @@ -3,65 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Product\ProductList; /** - * Test class for \Magento\Catalog\Block\Product\List\Upsell. + * Check the correct behavior of up-sell products on the product view page * - * @magentoDataFixture Magento/Catalog/_files/products_upsell.php + * @see \Magento\Catalog\Block\Product\ProductList\Upsell * @magentoDbIsolation disabled + * @magentoAppArea frontend */ -class UpsellTest extends \PHPUnit\Framework\TestCase +class UpsellTest extends AbstractLinksTest { - /** - * @var \Magento\Catalog\Block\Product\ProductList\Upsell - */ + /** @var Upsell */ protected $block; /** - * @var \Magento\Catalog\Api\Data\ProductInterface + * @inheritdoc */ - protected $product; - - /** - * @var \Magento\Catalog\Api\Data\ProductInterface - */ - protected $upsellProduct; - protected function setUp() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - - $this->upsellProduct = $productRepository->get('simple'); - $this->product = $productRepository->get('simple_with_upsell'); - $objectManager->get(\Magento\Framework\Registry::class)->register('product', $this->product); + parent::setUp(); - $this->block = $objectManager->get(\Magento\Framework\View\LayoutInterface::class) - ->createBlock(\Magento\Catalog\Block\Product\ProductList\Upsell::class); - - $this->block->setLayout($objectManager->get(\Magento\Framework\View\LayoutInterface::class)); - $this->block->setTemplate('Magento_Catalog::product/list/items.phtml'); - $this->block->setType('upsell'); - $this->block->addChild('addto', \Magento\Catalog\Block\Product\ProductList\Item\Container::class); - $this->block->getChildBlock( - 'addto' - )->addChild( - 'compare', - \Magento\Catalog\Block\Product\ProductList\Item\AddTo\Compare::class, - ['template' => 'Magento_Catalog::product/list/addto/compare.phtml'] - ); + $this->block = $this->layout->createBlock(Upsell::class); + $this->linkType = 'upsell'; } /** - * @magentoAppIsolation enabled + * Checks for a up-sell product when block code is generated + * + * @magentoDataFixture Magento/Catalog/_files/products_upsell.php + * @return void */ - public function testAll() + public function testAll(): void { + $this->product = $this->productRepository->get('simple_with_upsell'); + $this->block->setProduct($this->product); + $this->prepareBlock(); $html = $this->block->toHtml(); $this->assertNotEmpty($html); $this->assertContains('Simple Up Sell', $html); @@ -69,12 +48,93 @@ public function testAll() } /** - * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/products_upsell.php + * @return void */ - public function testGetIdentities() + public function testGetIdentities(): void { - $expectedTags = ['cat_p_' . $this->upsellProduct->getId(), 'cat_p']; + $upsellProduct = $this->productRepository->get('simple'); + $this->product = $this->productRepository->get('simple_with_upsell'); + $this->block->setProduct($this->product); + $this->prepareBlock(); + $expectedTags = ['cat_p_' . $upsellProduct->getId(), 'cat_p']; $tags = $this->block->getIdentities(); $this->assertEquals($expectedTags, $tags); } + + /** + * Test the display of up-sell products in the block + * + * @dataProvider displayLinkedProductsProvider + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @param array $data + * @return void + */ + public function testDisplayUpsellProducts(array $data): void + { + $this->updateProducts($data['updateProducts']); + $this->linkProducts('simple', $this->existingProducts); + $this->product = $this->productRepository->get('simple'); + $this->block->setProduct($this->product); + $items = $this->block->getItems(); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected up-sell products do not match actual up-sell products!' + ); + } + + /** + * Test the position of up-sell products in the block + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @return void + */ + public function testPositionUpsellProducts(): void + { + $data = $this->getPositionData(); + $this->linkProducts('simple', $data['productLinks']); + $this->product = $this->productRepository->get('simple'); + $this->block->setProduct($this->product); + $items = $this->block->getItems(); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected up-sell products do not match actual up-sell products!' + ); + } + + /** + * Test the display of up-sell products in the block on different websites + * + * @dataProvider multipleWebsitesLinkedProductsProvider + * @magentoDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @magentoAppIsolation enabled + * @param array $data + * @return void + */ + public function testMultipleWebsitesUpsellProducts(array $data): void + { + $this->updateProducts($this->prepareWebsiteIdsProducts()); + $productLinks = array_replace_recursive($this->existingProducts, $data['productLinks']); + $this->linkProducts('simple-1', $productLinks); + $this->product = $this->productRepository->get( + 'simple-1', + false, + $this->storeManager->getStore($data['storeCode'])->getId() + ); + $this->block->setProduct($this->product); + $items = $this->block->getItems(); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected up-sell products do not match actual up-sell products!' + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php new file mode 100644 index 0000000000000..9bcdb00eebe7c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -0,0 +1,357 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\LayoutInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** + * Provide tests for displaying images on product page. + * + * @magentoAppArea frontend + */ +class GalleryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductResource + */ + private $productResource; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var Json + */ + private $serializer; + + /** + * @var Gallery + */ + private $block; + + /** + * @var array + */ + private $imageExpectation = [ + 'thumb' => '/m/a/magento_image.jpg', + 'img' => '/m/a/magento_image.jpg', + 'full' => '/m/a/magento_image.jpg', + 'caption' => 'Image Alt Text', + 'position' => '1', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + /** + * @var array + */ + private $thumbnailExpectation = [ + 'thumb' => '/m/a/magento_thumbnail.jpg', + 'img' => '/m/a/magento_thumbnail.jpg', + 'full' => '/m/a/magento_thumbnail.jpg', + 'caption' => 'Thumbnail Image', + 'position' => '2', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + /** + * @var array + */ + private $placeholderExpectation = [ + 'thumb' => '/placeholder/thumbnail.jpg', + 'img' => '/placeholder/image.jpg', + 'full' => '/placeholder/image.jpg', + 'caption' => '', + 'position' => '0', + 'isMain' => true, + 'type' => 'image', + 'videoUrl' => null, + ]; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->productResource = $this->objectManager->get(ProductResource::class); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + $this->serializer = $this->objectManager->get(Json::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Gallery::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation enabled + * @return void + */ + public function testGetGalleryImagesJsonWithoutImages(): void + { + $this->block->setData('product', $this->getProduct()); + $result = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + $this->assertImages(reset($result), $this->placeholderExpectation); + } + + /** + * @dataProvider galleryDisabledImagesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDbIsolation enabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJsonWithDisabledImage(array $images, array $expectation): void + { + $product = $this->getProduct(); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct()); + $firstImage = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + $this->assertImages(reset($firstImage), $expectation); + } + + /** + * @dataProvider galleryDisabledImagesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDbIsolation disabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJsonOnStoreWithDisabledImage(array $images, array $expectation): void + { + $secondStoreId = (int)$this->storeRepository->get('fixture_second_store')->getId(); + $product = $this->getProduct($secondStoreId); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct($secondStoreId)); + $firstImage = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + $this->assertImages(reset($firstImage), $expectation); + } + + /** + * @return array + */ + public function galleryDisabledImagesDataProvider(): array + { + return [ + [ + 'images' => [ + '/m/a/magento_image.jpg' => ['disabled' => true], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => $this->thumbnailExpectation, + ], + ]; + } + + /** + * @dataProvider galleryImagesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDbIsolation enabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJson(array $images, array $expectation): void + { + $product = $this->getProduct(); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct()); + [$firstImage, $secondImage] = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + [$firstExpectedImage, $secondExpectedImage] = $expectation; + $this->assertImages($firstImage, $firstExpectedImage); + $this->assertImages($secondImage, $secondExpectedImage); + } + + /** + * @return array + */ + public function galleryImagesDataProvider(): array + { + return [ + 'with_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => ['main' => true], + ], + 'expectation' => [ + $this->imageExpectation, + array_merge($this->thumbnailExpectation, ['isMain' => true]), + ], + ], + 'without_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + array_merge($this->imageExpectation, ['isMain' => true]), + $this->thumbnailExpectation, + ], + ], + 'with_changed_position' => [ + 'images' => [ + '/m/a/magento_image.jpg' => ['position' => '2'], + '/m/a/magento_thumbnail.jpg' => ['position' => '1'], + ], + 'expectation' => [ + array_merge($this->thumbnailExpectation, ['position' => '1']), + array_merge($this->imageExpectation, ['position' => '2', 'isMain' => true]), + ], + ], + ]; + } + + /** + * @dataProvider galleryImagesOnStoreViewDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDbIsolation disabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJsonOnStoreView(array $images, array $expectation): void + { + $secondStoreId = (int)$this->storeRepository->get('fixture_second_store')->getId(); + $product = $this->getProduct($secondStoreId); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct($secondStoreId)); + [$firstImage, $secondImage] = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + [$firstExpectedImage, $secondExpectedImage] = $expectation; + $this->assertImages($firstImage, $firstExpectedImage); + $this->assertImages($secondImage, $secondExpectedImage); + } + + /** + * @return array + */ + public function galleryImagesOnStoreViewDataProvider(): array + { + return [ + 'with_store_labels' => [ + 'images' => [ + '/m/a/magento_image.jpg' => ['label' => 'Some store label'], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + array_merge($this->imageExpectation, ['isMain' => true, 'caption' => 'Some store label']), + $this->thumbnailExpectation, + ], + ], + 'with_changed_position' => [ + 'images' => [ + '/m/a/magento_image.jpg' => ['position' => '3'], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + array_merge($this->thumbnailExpectation, ['position' => '2']), + array_merge($this->imageExpectation, ['position' => '3', 'isMain' => true]), + ], + ], + 'with_main_store_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => ['main' => true], + ], + 'expectation' => [ + $this->imageExpectation, + array_merge($this->thumbnailExpectation, ['isMain' => true]), + ], + ], + ]; + } + + /** + * Updates product gallery images and saves product. + * + * @param ProductInterface $product + * @param array $images + * @param int|null $storeId + * @return void + */ + private function setGalleryImages(ProductInterface $product, array $images, int $storeId = null): void + { + $product->setImage(null); + foreach ($images as $file => $data) { + $mediaGalleryData = $product->getData('media_gallery'); + foreach ($mediaGalleryData['images'] as &$image) { + if ($image['file'] == $file) { + foreach ($data as $key => $value) { + $image[$key] = $value; + } + } + } + + $product->setData('media_gallery', $mediaGalleryData); + + if (!empty($data['main'])) { + $product->setImage($file); + } + } + + if ($storeId) { + $product->setStoreId($storeId); + } + + $this->productResource->save($product); + } + + /** + * Returns current product. + * + * @param int|null $storeId + * @return ProductInterface + */ + private function getProduct(?int $storeId = null): ProductInterface + { + return $this->productRepository->get('simple', false, $storeId, true); + } + + /** + * Asserts gallery image data. + * + * @param array $image + * @param array $expectedImage + * @return void + */ + private function assertImages(array $image, array $expectedImage): void + { + $this->assertStringEndsWith($expectedImage['thumb'], $image['thumb']); + $this->assertStringEndsWith($expectedImage['img'], $image['img']); + $this->assertStringEndsWith($expectedImage['full'], $image['full']); + $this->assertEquals($expectedImage['caption'], $image['caption']); + $this->assertEquals($expectedImage['position'], $image['position']); + $this->assertEquals($expectedImage['isMain'], $image['isMain']); + $this->assertEquals($expectedImage['type'], $image['type']); + $this->assertEquals($expectedImage['videoUrl'], $image['videoUrl']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ViewTest.php index 01398213b854a..59582f313cf55 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ViewTest.php @@ -3,94 +3,273 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Product; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Block\Product\View\Description; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** - * Test class for \Magento\Catalog\Block\Product\View. + * Checks product view block. * + * @see \Magento\Catalog\Block\Product\View * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation enabled */ -class ViewTest extends \PHPUnit\Framework\TestCase +class ViewTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var View */ + private $block; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Registry */ + private $registry; + + /** @var LayoutInterface */ + private $layout; + + /** @var Json */ + private $json; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var Description */ + private $descriptionBlock; + + /** @var array */ + private const SHORT_DESCRIPTION_BLOCK_DATA = [ + 'at_call' => 'getShortDescription', + 'at_code' => 'short_description', + 'overview' => 'overview', + 'at_label' => 'none', + 'title' => 'Overview', + 'add_attribute' => 'description', + ]; + /** - * @var \Magento\Catalog\Block\Product\View + * @inheritdoc */ - protected $_block; + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(View::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->json = $this->objectManager->get(Json::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->descriptionBlock = $this->layout->createBlock(Description::class); + } /** - * @var \Magento\Catalog\Model\Product + * @return void */ - protected $_product; + public function testSetLayout(): void + { + $productView = $this->layout->createBlock(View::class); - protected function setUp() + $this->assertInstanceOf(LayoutInterface::class, $productView->getLayout()); + } + + /** + * @return void + */ + public function testGetProduct(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_block = $objectManager->create(\Magento\Catalog\Block\Product\View::class); + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + + $this->assertNotEmpty($this->block->getProduct()->getId()); + $this->assertEquals($product->getId(), $this->block->getProduct()->getId()); - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->_product = $productRepository->get('simple'); + $this->registry->unregister('product'); + $this->block->setProductId($product->getId()); - $objectManager->get(\Magento\Framework\Registry::class)->unregister('product'); - $objectManager->get(\Magento\Framework\Registry::class)->register('product', $this->_product); + $this->assertEquals($product->getId(), $this->block->getProduct()->getId()); } - public function testSetLayout() + /** + * @return void + */ + public function testCanEmailToFriend(): void + { + $this->assertFalse($this->block->canEmailToFriend()); + } + + /** + * @return void + */ + public function testGetAddToCartUrl(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $product = $this->productRepository->get('simple'); + $url = $this->block->getAddToCartUrl($product); - /** @var $layout \Magento\Framework\View\Layout */ - $layout = $objectManager->get(\Magento\Framework\View\LayoutInterface::class); + $this->assertStringMatchesFormat( + '%scheckout/cart/add/%sproduct/' . $product->getId() . '/', + $url + ); + } - $productView = $layout->createBlock(\Magento\Catalog\Block\Product\View::class); + /** + * @return void + */ + public function testGetJsonConfig(): void + { + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + $config = $this->json->unserialize($this->block->getJsonConfig()); - $this->assertInstanceOf(\Magento\Framework\View\LayoutInterface::class, $productView->getLayout()); + $this->assertNotEmpty($config); + $this->assertArrayHasKey('productId', $config); + $this->assertEquals($product->getId(), $config['productId']); } - public function testGetProduct() + /** + * @return void + */ + public function testHasOptions(): void { - $this->assertNotEmpty($this->_block->getProduct()->getId()); - $this->assertEquals($this->_product->getId(), $this->_block->getProduct()->getId()); - - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Framework\Registry::class)->unregister('product'); - $this->_block->setProductId($this->_product->getId()); - $this->assertEquals($this->_product->getId(), $this->_block->getProduct()->getId()); + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + + $this->assertTrue($this->block->hasOptions()); } - public function testCanEmailToFriend() + /** + * @return void + */ + public function testHasRequiredOptions(): void { - $this->assertFalse($this->_block->canEmailToFriend()); + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + + $this->assertTrue($this->block->hasRequiredOptions()); + } + + /** + * @return void + */ + public function testStartBundleCustomization(): void + { + $this->markTestSkipped("Functionality not implemented in Magento 1.x. Implemented in Magento 2"); + + $this->assertFalse($this->block->startBundleCustomization()); } - public function testGetAddToCartUrl() + /** + * @magentoAppArea frontend + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + */ + public function testAddToCartBlockInvisibility(): void { - $url = $this->_block->getAddToCartUrl($this->_product); - $this->assertStringMatchesFormat('%scheckout/cart/add/%sproduct/' . $this->_product->getId() . '/', $url); + $outOfStockProduct = $this->productRepository->get('simple-out-of-stock'); + $this->registerProduct($outOfStockProduct); + $this->block->setTemplate('Magento_Catalog::product/view/addtocart.phtml'); + $output = $this->block->toHtml(); + + $this->assertNotContains((string)__('Add to Cart'), $output); } - public function testGetJsonConfig() + /** + * @magentoAppArea frontend + */ + public function testAddToCartBlockVisibility(): void { - $config = (array)json_decode($this->_block->getJsonConfig()); - $this->assertNotEmpty($config); - $this->assertArrayHasKey('productId', $config); - $this->assertEquals($this->_product->getId(), $config['productId']); + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + $this->block->setTemplate('Magento_Catalog::product/view/addtocart.phtml'); + $output = $this->block->toHtml(); + + $this->assertContains((string)__('Add to Cart'), $output); } - public function testHasOptions() + /** + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * @magentoDataFixture Magento/Catalog/_files/product_multistore_different_short_description.php + * @return void + */ + public function testProductShortDescription(): void + { + $product = $this->productRepository->get('simple-different-short-description'); + $currentStoreId = $this->storeManager->getStore()->getId(); + $output = $this->renderDescriptionBlock($product); + + $this->assertContains('First store view short description', $output); + + $secondStore = $this->storeManager->getStore('fixturestore'); + $this->storeManager->setCurrentStore($secondStore->getId()); + + try { + $product = $this->productRepository->get( + 'simple-different-short-description', + false, + $secondStore->getId(), + true + ); + $newBlockOutput = $this->renderDescriptionBlock($product, true); + + $this->assertContains('Second store view short description', $newBlockOutput); + } finally { + $this->storeManager->setCurrentStore($currentStoreId); + } + } + + /** + * @param ProductInterface $product + * @param bool $refreshBlock + * @return string + */ + private function renderDescriptionBlock(ProductInterface $product, bool $refreshBlock = false): string { - $this->assertTrue($this->_block->hasOptions()); + $this->registerProduct($product); + $descriptionBlock = $this->getDescriptionBlock($refreshBlock); + $descriptionBlock->addData(self::SHORT_DESCRIPTION_BLOCK_DATA); + $descriptionBlock->setTemplate('Magento_Catalog::product/view/attribute.phtml'); + + return $this->descriptionBlock->toHtml(); } - public function testHasRequiredOptions() + /** + * Get description block + * + * @param bool $refreshBlock + * @return Description + */ + private function getDescriptionBlock(bool $refreshBlock): Description { - $this->assertTrue($this->_block->hasRequiredOptions()); + if ($refreshBlock) { + $this->descriptionBlock = $this->layout->createBlock(Description::class); + } + + return $this->descriptionBlock; } - public function testStartBundleCustomization() + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void { - $this->markTestSkipped("Functionality not implemented in Magento 1.x. Implemented in Magento 2"); - $this->assertFalse($this->_block->startBundleCustomization()); + $this->registry->unregister('product'); + $this->registry->register('product', $product); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/DeleteTest.php new file mode 100644 index 0000000000000..8db16fa2c4546 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/DeleteTest.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for class \Magento\Catalog\Controller\Adminhtml\Category\Delete + * + * @magentoAppArea adminhtml + */ +class DeleteTest extends AbstractBackendController +{ + /** + * @return void + */ + public function testDeleteMissingCategory(): void + { + $incorrectId = 825852; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['id' => $incorrectId]); + $this->dispatch('backend/catalog/category/delete'); + $this->assertSessionMessages( + $this->equalTo([(string)__(sprintf('No such entity with id = %s', $incorrectId))]), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/category.php + */ + public function testDeleteCategory(): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['id' => 333]); + $this->dispatch('backend/catalog/category/delete'); + $this->assertSessionMessages($this->equalTo([(string)__('You deleted the category.')])); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UrlRewriteTest.php new file mode 100644 index 0000000000000..90f354d90f17a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UrlRewriteTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category\Save; + +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Class defines url rewrite creation for category save controller + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class UrlRewriteTest extends AbstractBackendController +{ + /** @var $urlRewriteCollectionFactory */ + private $urlRewriteCollectionFactory; + + /** @var Json */ + private $jsonSerializer; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->urlRewriteCollectionFactory = $this->_objectManager->get(UrlRewriteCollectionFactory::class); + $this->jsonSerializer = $this->_objectManager->get(Json::class); + } + + /** + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @dataProvider categoryDataProvider + * @param array $data + * @return void + */ + public function testUrlRewrite(array $data): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($data); + $this->dispatch('backend/catalog/category/save'); + $categoryId = $this->jsonSerializer->unserialize($this->getResponse()->getBody())['category']['entity_id']; + $this->assertNotNull($categoryId, 'The category was not created'); + $urlRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $urlRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_ID, ['eq' => $categoryId]) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE]); + $this->assertCount( + 1, + $urlRewriteCollection->getItems(), + 'Wrong count of url rewrites was created' + ); + } + + /** + * @return array + */ + public function categoryDataProvider(): array + { + return [ + 'url_rewrite_is_created_during_category_save' => [ + [ + 'path' => '1/2', + 'name' => 'Custom Name', + 'parent' => 2, + 'is_active' => '0', + 'include_in_menu' => '1', + 'display_mode' => 'PRODUCTS', + 'is_anchor' => true, + 'return_session_messages_only' => true, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'filter_price_range' => 1, + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 1001d58ee8a67..e89e5aae92cf5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -3,22 +3,62 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Framework\Acl\Builder; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\Store; -use Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\CategoryFactory as CategoryModelFactory; /** + * Test for admin category functionality. + * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CategoryTest extends \Magento\TestFramework\TestCase\AbstractBackendController +class CategoryTest extends AbstractBackendController { /** - * @var \Magento\Catalog\Model\ResourceModel\Product + * @var ProductResource */ protected $productResource; + /** + * @var Builder + */ + private $aclBuilder; + + /** + * @var CategoryModelFactory + */ + private $categoryFactory; + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var Json + */ + private $json; /** * @inheritDoc @@ -29,13 +69,20 @@ protected function setUp() { parent::setUp(); - /** @var Product $productResource */ + /** @var ProductResource $productResource */ $this->productResource = Bootstrap::getObjectManager()->get( - Product::class + ProductResource::class ); + $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); + $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryModelFactory::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeRepository = $this->_objectManager->get(StoreRepositoryInterface::class); + $this->json = $this->_objectManager->get(Json::class); } /** + * Test save action. + * * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDbIsolation enabled * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 @@ -43,35 +90,24 @@ protected function setUp() * @param array $inputData * @param array $defaultAttributes * @param array $attributesSaved - * @param bool $isSuccess + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - public function testSaveAction($inputData, $defaultAttributes, $attributesSaved = [], $isSuccess = true) + public function testSaveAction(array $inputData, array $defaultAttributes, array $attributesSaved = []): void { - /** @var $store \Magento\Store\Model\Store */ - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); + $store = $this->storeRepository->get('fixturestore'); $storeId = $store->getId(); - $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->getRequest()->setParam('store', $storeId); $this->getRequest()->setParam('id', 2); $this->dispatch('backend/catalog/category/save'); - - if ($isSuccess) { - $this->assertSessionMessages( - $this->equalTo(['You saved the category.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - } - - /** @var $category \Magento\Catalog\Model\Category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class + $this->assertSessionMessages( + $this->equalTo(['You saved the category.']), + MessageInterface::TYPE_SUCCESS ); - $category->setStoreId($storeId); - $category->load(2); - + /** @var $category Category */ + $category = $this->categoryRepository->get(2, $storeId); $errors = []; foreach ($attributesSaved as $attribute => $value) { $actualValue = $category->getData($attribute); @@ -95,11 +131,56 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved } /** + * Check default value for category url path + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories.php + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testDefaultValueForCategoryUrlPath(): void + { + $categoryId = 3; + $category = $this->categoryRepository->get($categoryId); + $newUrlPath = 'test_url_path'; + $defaultUrlPath = $category->getData('url_path'); + + // update url_path and check it + $category->setStoreId(1); + $category->setUrlKey($newUrlPath); + $category->setUrlPath($newUrlPath); + $this->categoryRepository->save($category); + $this->assertEquals($newUrlPath, $category->getUrlPath()); + + // set default url_path and check it + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $postData = $category->getData(); + $postData['use_default'] = + [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'url_key' => 1, + ]; + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the category.')]), + MessageInterface::TYPE_SUCCESS + ); + $category = $this->categoryRepository->get($categoryId); + $this->assertEquals($defaultUrlPath, $category->getData('url_key')); + } + + /** + * Test save action from product form page + * * @param array $postData * @dataProvider categoryCreatedFromProductCreationPageDataProvider * @magentoDbIsolation enabled + * @return void */ - public function testSaveActionFromProductCreationPage($postData) + public function testSaveActionFromProductCreationPage(array $postData): void { $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); @@ -112,11 +193,7 @@ public function testSaveActionFromProductCreationPage($postData) $this->stringContains('http://localhost/index.php/backend/catalog/category/edit/') ); } else { - $result = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Json\Helper\Data::class - )->jsonDecode( - $body - ); + $result = $this->json->unserialize($body); $this->assertArrayHasKey('messages', $result); $this->assertFalse($result['error']); $category = $result['category']; @@ -130,10 +207,12 @@ public function testSaveActionFromProductCreationPage($postData) } /** + * Get category post data + * * @static * @return array */ - public static function categoryCreatedFromProductCreationPageDataProvider() + public static function categoryCreatedFromProductCreationPageDataProvider(): array { /* Keep in sync with new-category-dialog.js */ $postData = [ @@ -152,8 +231,10 @@ public static function categoryCreatedFromProductCreationPageDataProvider() /** * Test SuggestCategories finds any categories. + * + * @return void */ - public function testSuggestCategoriesActionDefaultCategoryFound() + public function testSuggestCategoriesActionDefaultCategoryFound(): void { $this->getRequest()->setParam('label_part', 'Default'); $this->dispatch('backend/catalog/category/suggestCategories'); @@ -165,8 +246,10 @@ public function testSuggestCategoriesActionDefaultCategoryFound() /** * Test SuggestCategories properly processes search by label. + * + * @return void */ - public function testSuggestCategoriesActionNoSuggestions() + public function testSuggestCategoriesActionNoSuggestions(): void { $this->getRequest()->setParam('label_part', strrev('Default')); $this->dispatch('backend/catalog/category/suggestCategories'); @@ -174,10 +257,12 @@ public function testSuggestCategoriesActionNoSuggestions() } /** + * Save action data provider + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @return array */ - public function saveActionDataProvider() + public function saveActionDataProvider(): array { return [ 'default values' => [ @@ -207,7 +292,7 @@ public function saveActionDataProvider() 'custom_design_from' => 1, 'custom_design_to' => 1, 'page_layout' => 1, - 'custom_layout_update' => 1, + 'custom_layout_update' => null, ], ], [ @@ -253,7 +338,6 @@ public function saveActionDataProvider() 'custom_design_from' => '5/21/2015', 'custom_design_to' => '5/29/2015', 'page_layout' => '', - 'custom_layout_update' => '', 'use_config' => [ 'available_sort_by' => 1, 'default_sort_by' => 1, @@ -275,7 +359,6 @@ public function saveActionDataProvider() 'description' => true, 'meta_keywords' => true, 'meta_description' => true, - 'custom_layout_update' => true, 'custom_design_from' => true, 'custom_design_to' => true, 'filter_price_range' => false @@ -295,70 +378,44 @@ public function saveActionDataProvider() 'description' => 'Custom Description', 'meta_keywords' => 'Custom keywords', 'meta_description' => 'Custom meta description', - 'custom_layout_update' => null, 'custom_design_from' => '2015-05-21 00:00:00', 'custom_design_to' => '2015-05-29 00:00:00', 'filter_price_range' => null ], ], - 'incorrect datefrom' => [ - [ - 'id' => '2', - 'entity_id' => '2', - 'path' => '1/2', - 'name' => 'Custom Name', - 'is_active' => '0', - 'description' => 'Custom Description', - 'meta_title' => 'Custom Title', - 'meta_keywords' => 'Custom keywords', - 'meta_description' => 'Custom meta description', - 'include_in_menu' => '0', - 'url_key' => 'default-category', - 'display_mode' => 'PRODUCTS', - 'landing_page' => '1', - 'is_anchor' => true, - 'custom_apply_to_products' => '0', - 'custom_design' => 'Magento/blank', - 'custom_design_from' => '5/29/2015', - 'custom_design_to' => '5/21/2015', - 'page_layout' => '', - 'custom_layout_update' => '', - 'use_config' => [ - 'available_sort_by' => 1, - 'default_sort_by' => 1, - 'filter_price_range' => 1, - ], - ], - [ - 'name' => false, - 'default_sort_by' => false, - 'display_mode' => false, - 'meta_title' => false, - 'custom_design' => false, - 'page_layout' => false, - 'is_active' => false, - 'include_in_menu' => false, - 'landing_page' => false, - 'custom_apply_to_products' => false, - 'available_sort_by' => false, - 'description' => false, - 'meta_keywords' => false, - 'meta_description' => false, - 'custom_layout_update' => false, - 'custom_design_from' => false, - 'custom_design_to' => false, - 'filter_price_range' => false - ], - [], - false - ] ]; } + /** + * @magentoDbIsolation enabled + * @return void + */ + public function testIncorrectDateFrom(): void + { + $data = [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + 'custom_design_from' => '5/29/2015', + 'custom_design_to' => '5/21/2015', + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($data); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('Make sure the To Date is later than or the same as the From Date.')]), + MessageInterface::TYPE_ERROR + ); + } + /** * Test validation. + * + * @return void */ - public function testSaveActionCategoryWithDangerRequest() + public function testSaveActionCategoryWithDangerRequest(): void { $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( @@ -377,11 +434,13 @@ public function testSaveActionCategoryWithDangerRequest() $this->dispatch('backend/catalog/category/save'); $this->assertSessionMessages( $this->equalTo(['The "Name" attribute value is empty. Set the attribute and try again.']), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR + MessageInterface::TYPE_ERROR ); } /** + * Test move action. + * * @magentoDataFixture Magento/Catalog/_files/category_tree.php * @dataProvider moveActionDataProvider * @@ -391,18 +450,23 @@ public function testSaveActionCategoryWithDangerRequest() * @param int $grandChildId * @param string $grandChildUrlKey * @param boolean $error + * @return void */ - public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, $grandChildUrlKey, $error) - { + public function testMoveAction( + int $parentId, + int $childId, + string $childUrlKey, + int $grandChildId, + string $grandChildUrlKey, + bool $error + ): void { $urlKeys = [ $childId => $childUrlKey, $grandChildId => $grandChildUrlKey, ]; foreach ($urlKeys as $categoryId => $urlKey) { - /** @var $category \Magento\Catalog\Model\Category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); + /** @var $category Category */ + $category = $this->categoryFactory->create(); if ($categoryId > 0) { $category->load($categoryId) ->setUrlKey($urlKey) @@ -414,15 +478,17 @@ public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, ->setPostValue('pid', $parentId) ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/category/move'); - $jsonResponse = json_decode($this->getResponse()->getBody()); + $jsonResponse = $this->json->unserialize($this->getResponse()->getBody()); $this->assertNotNull($jsonResponse); - $this->assertEquals($error, $jsonResponse->error); + $this->assertEquals($error, $jsonResponse['error']); } /** + * Move action data provider + * * @return array */ - public function moveActionDataProvider() + public function moveActionDataProvider(): array { return [ [400, 401, 'first_url_key', 402, 'second_url_key', false], @@ -433,17 +499,17 @@ public function moveActionDataProvider() } /** + * Test save category with product position. + * * @magentoDataFixture Magento/Catalog/_files/products_in_different_stores.php * @magentoDbIsolation disabled * @dataProvider saveActionWithDifferentWebsitesDataProvider * * @param array $postData */ - public function testSaveCategoryWithProductPosition(array $postData) + public function testSaveCategoryWithProductPosition(array $postData): void { - /** @var $store \Magento\Store\Model\Store */ - $store = Bootstrap::getObjectManager()->create(Store::class); - $store->load('fixturestore', 'code'); + $store = $this->storeRepository->get('fixturestore'); $storeId = $store->getId(); $oldCategoryProductsCount = $this->getCategoryProductsCount(); $this->getRequest()->setParam('store', $storeId); @@ -451,6 +517,10 @@ public function testSaveCategoryWithProductPosition(array $postData) $this->getRequest()->setParam('id', 96377); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the category.')]), + MessageInterface::TYPE_SUCCESS + ); $newCategoryProductsCount = $this->getCategoryProductsCount(); $this->assertEquals( $oldCategoryProductsCount, @@ -460,10 +530,12 @@ public function testSaveCategoryWithProductPosition(array $postData) } /** + * Save action data provider + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @return array */ - public function saveActionWithDifferentWebsitesDataProvider() + public function saveActionWithDifferentWebsitesDataProvider(): array { return [ 'default_values' => [ @@ -477,7 +549,6 @@ public function saveActionWithDifferentWebsitesDataProvider() 'path' => '1/2/96377', 'level' => '2', 'children_count' => '0', - 'row_id' => '96377', 'name' => 'Category 1', 'display_mode' => 'PRODUCTS', 'url_key' => 'category-1', @@ -541,7 +612,7 @@ public function saveActionWithDifferentWebsitesDataProvider() } /** - * Get items count from catalog_category_product + * Get items count from catalog_category_product. * * @return int */ @@ -555,4 +626,268 @@ private function getCategoryProductsCount(): int $this->productResource->getConnection()->fetchAll($oldCategoryProducts) ); } + + /** + * Check whether additional authorization is required for the design fields. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @throws \Throwable + * @return void + */ + public function testSaveDesign(): void + { + /** @var $store \Magento\Store\Model\Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); + $store->load('fixturestore', 'code'); + $storeId = $store->getId(); + $requestData = [ + 'id' => '2', + 'entity_id' => '2', + 'path' => '1/2', + 'name' => 'Custom Name', + 'is_active' => '0', + 'description' => 'Custom Description', + 'meta_title' => 'Custom Title', + 'meta_keywords' => 'Custom keywords', + 'meta_description' => 'Custom meta description', + 'include_in_menu' => '0', + 'url_key' => 'default-test-category', + 'display_mode' => 'PRODUCTS', + 'landing_page' => '1', + 'is_anchor' => true, + 'store_id' => $storeId, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'filter_price_range' => 1, + ], + ]; + $uri = 'backend/catalog/category/save'; + + //Trying to update the category's design settings without proper permissions. + //Expected list of sessions messages collected throughout the controller calls. + $sessionMessages = ['Not allowed to edit the category\'s design attributes']; + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_category_design'); + $requestData['custom_layout_update_file'] = 'test-file'; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('store', $requestData['store_id']); + $this->getRequest()->setParam('id', $requestData['id']); + $this->dispatch($uri); + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + + //Trying again with the permissions. + $requestData['custom_layout_update_file'] = null; + $requestData['page_layout'] = '2columns-left'; + $this->aclBuilder->getAcl() + ->allow(null, ['Magento_Catalog::categories', 'Magento_Catalog::edit_category_design']); + $this->getRequest()->setDispatched(false); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('store', $requestData['store_id']); + $this->getRequest()->setParam('id', $requestData['id']); + $this->dispatch($uri); + /** @var CategoryModel $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + $this->assertEquals('2columns-left', $category->getData('page_layout')); + //No new error messages + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + + //Trying to save special value without the permissions. + $requestData['custom_layout_update_file'] = CategoryModel\Attribute\Backend\LayoutUpdate::VALUE_USE_UPDATE_XML; + $requestData['description'] = 'test'; + $this->aclBuilder->getAcl()->deny(null, ['Magento_Catalog::edit_category_design']); + $this->getRequest()->setDispatched(false); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('store', $requestData['store_id']); + $this->getRequest()->setParam('id', $requestData['id']); + $this->dispatch($uri); + /** @var CategoryModel $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + $this->assertEquals('2columns-left', $category->getData('page_layout')); + $this->assertEmpty($category->getData('custom_layout_update_file')); + $this->assertEquals('test', $category->getData('description')); + //No new error messages + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Save design attributes with default values without design permissions. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @return void + * @throws \Throwable + */ + public function testSaveDesignWithDefaults(): void + { + /** @var $store \Magento\Store\Model\Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); + $store->load('fixturestore', 'code'); + $storeId = $store->getId(); + /** @var CategoryModel $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + $attributes = $category->getAttributes(); + $attributes['custom_design']->setDefaultValue('1'); + $attributes['custom_design']->save(); + $requestData = [ + 'name' => 'Test name', + 'parent_id' => '2', + 'is_active' => '0', + 'description' => 'Custom Description', + 'meta_title' => 'Custom Title', + 'meta_keywords' => 'Custom keywords', + 'meta_description' => 'Custom meta description', + 'include_in_menu' => '0', + 'url_key' => 'default-test-category-test', + 'display_mode' => 'PRODUCTS', + 'landing_page' => '1', + 'is_anchor' => true, + 'store_id' => $storeId, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'filter_price_range' => 1, + ], + 'custom_design' => '1', + 'custom_apply_to_products' => '0' + ]; + $uri = 'backend/catalog/category/save'; + + //Updating the category's design settings without proper permissions. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_category_design'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('store', $requestData['store_id']); + $this->dispatch($uri); + + //Verifying that category was saved. + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $id = $registry->registry('current_category')->getId(); + /** @var CategoryModel $category */ + $category = $this->categoryFactory->create(); + $category->load($id); + $this->assertNotEmpty($category->getId()); + $this->assertEquals('1', $category->getData('custom_design')); + } + + /** + * Test custom update files functionality. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @throws \Throwable + * @return void + */ + public function testSaveCustomLayout(): void + { + $file = 'test_file'; + /** @var $store \Magento\Store\Model\Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); + /** @var CategoryLayoutUpdateManager $layoutManager */ + $layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); + $layoutManager->setCategoryFakeFiles(2, [$file]); + $store->load('fixturestore', 'code'); + $storeId = $store->getId(); + $requestData = [ + 'id' => '2', + 'entity_id' => '2', + 'path' => '1/2', + 'name' => 'Custom Name', + 'is_active' => '0', + 'description' => 'Custom Description', + 'meta_title' => 'Custom Title', + 'meta_keywords' => 'Custom keywords', + 'meta_description' => 'Custom meta description', + 'include_in_menu' => '0', + 'url_key' => 'default-test-category', + 'display_mode' => 'PRODUCTS', + 'landing_page' => '1', + 'is_anchor' => true, + 'store_id' => $storeId, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'filter_price_range' => 1, + ], + ]; + $uri = 'backend/catalog/category/save'; + + //Saving a wrong file + $requestData['custom_layout_update_file'] = $file . 'INVALID'; + $this->getRequest()->setDispatched(false); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('store', $requestData['store_id']); + $this->getRequest()->setParam('id', $requestData['id']); + $this->dispatch($uri); + + //Checking that the value is not saved + /** @var CategoryModel $category */ + $category = $this->categoryFactory->create(); + $category->load($requestData['entity_id']); + $this->assertEmpty($category->getData('custom_layout_update_file')); + + //Saving the correct file + $requestData['custom_layout_update_file'] = $file; + $this->getRequest()->setDispatched(false); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('store', $requestData['store_id']); + $this->getRequest()->setParam('id', $requestData['id']); + $this->dispatch($uri); + + //Checking that the value is saved + /** @var CategoryModel $category */ + $category = $this->categoryFactory->create(); + $category->load($requestData['entity_id']); + $this->assertEquals($file, $category->getData('custom_layout_update_file')); + } + + /** + * Verify that the category cannot be saved if the category url matches the admin url. + * + * @return void + * @magentoConfigFixture admin/url/use_custom_path 1 + * @magentoConfigFixture admin/url/custom_path backend + */ + public function testSaveWithCustomBackendNameAction(): void + { + /** @var FrontNameResolver $frontNameResolver */ + $frontNameResolver = Bootstrap::getObjectManager()->create(FrontNameResolver::class); + $urlKey = $frontNameResolver->getFrontName(); + $inputData = [ + 'id' => '2', + 'url_key' => $urlKey, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1 + ] + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($inputData); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo( + [ + 'URL key "backend" matches a reserved endpoint name ' + . '(admin, soap, rest, graphql, standard, backend). Use another URL key.' + ] + ), + MessageInterface::TYPE_ERROR + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index 3ec8c806dcbb1..2f35a5fdafc3a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -22,12 +22,15 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr protected function setUp() { - $this->publisherConsumerController = Bootstrap::getObjectManager()->create(PublisherConsumerController::class, [ - 'consumers' => $this->consumers, - 'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt", - 'maxMessages' => null, - 'appInitParams' => Bootstrap::getInstance()->getAppInitParams() - ]); + $this->publisherConsumerController = Bootstrap::getObjectManager()->create( + PublisherConsumerController::class, + [ + 'consumers' => $this->consumers, + 'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt", + 'maxMessages' => null, + 'appInitParams' => Bootstrap::getInstance()->getAppInitParams() + ] + ); try { $this->publisherConsumerController->startConsumers(); @@ -124,7 +127,7 @@ public function testSaveActionChangeVisibility($attributes) $this->publisherConsumerController->waitForAsynchronousResult( function () use ($repository) { - sleep(3); + sleep(10); // Should be refactored in the scope of MC-22947 return $repository->get( 'simple', false, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/AbstractSaveAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/AbstractSaveAttributeTest.php new file mode 100644 index 0000000000000..d0f1256f1fdb7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/AbstractSaveAttributeTest.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save\InputType; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Model\Store; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Base create and assert attribute data. + */ +abstract class AbstractSaveAttributeTest extends AbstractBackendController +{ + /** + * @var AttributeRepositoryInterface + */ + protected $attributeRepository; + + /** + * @var Escaper + */ + protected $escaper; + + /** + * @var Json + */ + protected $jsonSerializer; + + /** + * @var ProductAttributeOptionManagementInterface + */ + protected $productAttributeOptionManagement; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->attributeRepository = $this->_objectManager->get(AttributeRepositoryInterface::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + $this->jsonSerializer = $this->_objectManager->get(Json::class); + $this->productAttributeOptionManagement = $this->_objectManager->get( + ProductAttributeOptionManagementInterface::class + ); + } + + /** + * Create attribute via save product attribute controller and assert that attribute + * created correctly. + * + * @param array $attributeData + * @param array $checkArray + * @return void + */ + protected function createAttributeUsingDataAndAssert(array $attributeData, array $checkArray): void + { + $attributeCode = $this->getAttributeCodeFromAttributeData($attributeData); + if (isset($attributeData['serialized_options_arr'])) { + $attributeData['serialized_options'] = $this->serializeOptions($attributeData['serialized_options_arr']); + } + $this->createAttributeViaController($attributeData); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the product attribute.')]), + MessageInterface::TYPE_SUCCESS + ); + try { + $attribute = $this->attributeRepository->get(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode); + $this->assertAttributeData($attribute, $attributeData, $checkArray); + $this->attributeRepository->delete($attribute); + } catch (NoSuchEntityException $e) { + $this->fail("Attribute with code {$attributeCode} was not created."); + } + } + + /** + * Create attribute via save product attribute controller and assert that we have error during save process. + * + * @param array $attributeData + * @param string $errorMessage + * @return void + */ + protected function createAttributeUsingDataWithErrorAndAssert(array $attributeData, string $errorMessage): void + { + if (isset($attributeData['serialized_options_arr']) + && count($attributeData['serialized_options_arr']) + ) { + $attributeData['serialized_options'] = $this->serializeOptions($attributeData['serialized_options_arr']); + } + $this->createAttributeViaController($attributeData); + $this->assertSessionMessages( + $this->equalTo([$this->escaper->escapeHtml($errorMessage)]), + MessageInterface::TYPE_ERROR + ); + $attributeCode = $this->getAttributeCodeFromAttributeData($attributeData); + try { + $attribute = $this->attributeRepository->get(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode); + $this->attributeRepository->delete($attribute); + } catch (NoSuchEntityException $e) { + //Attribute already deleted. + } + } + + /** + * Assert that options was created. + * + * @param AttributeInterface $attribute + * @param array $optionsData + * @return void + */ + protected function assertAttributeOptions(AttributeInterface $attribute, array $optionsData): void + { + $attributeOptions = $this->productAttributeOptionManagement->getItems($attribute->getAttributeCode()); + foreach ($optionsData as $optionData) { + $valueItemArr = $optionData['option']['value']; + $optionLabel = reset($valueItemArr)[1]; + $optionFounded = false; + foreach ($attributeOptions as $attributeOption) { + if ($attributeOption->getLabel() === $optionLabel) { + $optionFounded = true; + break; + } + } + $this->assertTrue($optionFounded); + } + } + + /** + * Compare attribute data with data which we use for create attribute. + * + * @param AttributeInterface|AbstractAttribute $attribute + * @param array $attributeData + * @param array $checkData + * @return void + */ + private function assertAttributeData( + AttributeInterface $attribute, + array $attributeData, + array $checkData + ): void { + $frontendInput = $checkData['frontend_input'] ?? $attributeData['frontend_input']; + $this->assertEquals('Test attribute name', $attribute->getDefaultFrontendLabel()); + $this->assertEquals($frontendInput, $attribute->getFrontendInput()); + + if (isset($attributeData['serialized_options'])) { + $this->assertAttributeOptions($attribute, $attributeData['serialized_options_arr']); + } + + //Additional asserts + foreach ($checkData as $valueKey => $value) { + $this->assertEquals($value, $attribute->getDataUsingMethod($valueKey)); + } + } + + /** + * Get attribute code from attribute data. If attribute code doesn't exist in + * attribute data get attribute using default frontend label. + * + * @param array $attributeData + * @return string + */ + private function getAttributeCodeFromAttributeData(array $attributeData): string + { + $attributeCode = $attributeData['attribute_code'] ?? null; + if (!$attributeCode) { + $attributeCode = strtolower( + str_replace(' ', '_', $attributeData['frontend_label'][Store::DEFAULT_STORE_ID]) + ); + } + + return $attributeCode; + } + + /** + * Create attribute using catalog/product_attribute/save action. + * + * @param array $attributeData + * @return void + */ + private function createAttributeViaController(array $attributeData): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($attributeData); + $this->dispatch('backend/catalog/product_attribute/save'); + } + + /** + * Create serialized options string. + * + * @param array $optionsArr + * @return string + */ + private function serializeOptions(array $optionsArr): string + { + $resultArr = []; + + foreach ($optionsArr as $option) { + $resultArr[] = http_build_query($option); + } + + return $this->jsonSerializer->serialize($resultArr); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/MediaImageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/MediaImageTest.php new file mode 100644 index 0000000000000..f8adac2872773 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/MediaImageTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save\InputType; + +/** + * Test cases related to create attribute with input type media image. + * + * @magentoDbIsolation enabled + */ +class MediaImageTest extends AbstractSaveAttributeTest +{ + /** + * Test create attribute and compare attribute data and input data. + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Attribute\DataProvider\MediaImage::getAttributeDataWithCheckArray() + * + * @param array $attributePostData + * @param array $checkArray + * @return void + */ + public function testCreateAttribute(array $attributePostData, array $checkArray): void + { + $this->createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Attribute\DataProvider\MediaImage::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/PriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/PriceTest.php new file mode 100644 index 0000000000000..fb71f0a4d9d76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save/InputType/PriceTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save\InputType; + +/** + * Test cases related to create attribute with input type price. + * + * @magentoDbIsolation enabled + */ +class PriceTest extends AbstractSaveAttributeTest +{ + /** + * Test create attribute and compare attribute data and input data. + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Attribute\DataProvider\Price::getAttributeDataWithCheckArray() + * + * @param array $attributePostData + * @param array $checkArray + * @return void + */ + public function testCreateAttribute(array $attributePostData, array $checkArray): void + { + $this->createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Attribute\DataProvider\Price::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index e1d3e960593a9..967208ef800ff 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -136,32 +136,6 @@ public function testAttributeWithoutId() $this->assertEquals('You saved the product attribute.', $message->getText()); } - /** - * @return void - */ - public function testWrongAttributeCode() - { - $postData = $this->_getAttributeData() + ['attribute_code' => '_()&&&?']; - $this->getRequest()->setPostValue($postData); - $this->getRequest()->setMethod(HttpRequest::METHOD_POST); - $this->dispatch('backend/catalog/product_attribute/save'); - $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); - $this->assertContains( - 'catalog/product_attribute/edit', - $this->getResponse()->getHeader('Location')->getFieldValue() - ); - /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); - $this->assertEquals(1, $messages->getCountByType('error')); - /** @var \Magento\Framework\Message\Error $message */ - $message = $messages->getItemsByType('error')[0]; - $this->assertEquals( - 'Attribute code "_()&&&?" is invalid. Please use only letters (a-z or A-Z),' - . ' numbers (0-9) or underscore (_) in this field, and the first character should be a letter.', - $message->getText() - ); - } - /** * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php new file mode 100644 index 0000000000000..683fbc1a358c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php @@ -0,0 +1,278 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Gallery; + +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\App\Filesystem\DirectoryList as AppDirectoryList; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Provide tests for admin product upload image action. + * + * @magentoAppArea adminhtml + */ +class UploadTest extends AbstractBackendController +{ + /** + * @inheritdoc + */ + protected $resource = 'Magento_Catalog::products'; + + /** + * @inheritdoc + */ + protected $uri = 'backend/catalog/product_gallery/upload'; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var Json + */ + private $serializer; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var Config + */ + private $config; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->httpMethod = HttpRequest::METHOD_POST; + $this->filesystem = $this->_objectManager->get(Filesystem::class); + $this->serializer = $this->_objectManager->get(Json::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(AppDirectoryList::MEDIA); + $this->config = $this->_objectManager->get(Config::class); + } + + /** + * Test upload image on admin product page. + * + * @dataProvider uploadActionDataProvider + * @magentoDbIsolation enabled + * @param array $file + * @param array $expectation + * @return void + */ + public function testUploadAction(array $file, array $expectation): void + { + $this->copyFileToSysTmpDir($file); + $this->getRequest()->setMethod($this->httpMethod); + $this->dispatch($this->uri); + $jsonBody = $this->serializer->unserialize($this->getResponse()->getBody()); + $this->assertEquals($jsonBody['name'], $expectation['name']); + $this->assertEquals($jsonBody['type'], $expectation['type']); + $this->assertEquals($jsonBody['file'], $expectation['file']); + $this->assertEquals($jsonBody['url'], $expectation['url']); + $this->assertArrayNotHasKey('error', $jsonBody); + $this->assertArrayNotHasKey('errorcode', $jsonBody); + $this->assertFileExists( + $this->getFileAbsolutePath($expectation['tmp_media_path']) + ); + } + + /** + * @return array + */ + public function uploadActionDataProvider(): array + { + return [ + 'upload_image_with_type_jpg' => [ + 'file' => [ + 'name' => 'magento_image.jpg', + 'type' => 'image/jpeg', + 'current_path' => '/../../../../_files', + ], + 'expectation' => [ + 'name' => 'magento_image.jpg', + 'type' => 'image/jpeg', + 'file' => '/m/a/magento_image.jpg.tmp', + 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.jpg', + 'tmp_media_path' => '/m/a/magento_image.jpg', + ], + ], + 'upload_image_with_type_png' => [ + 'file' => [ + 'name' => 'product_image.png', + 'type' => 'image/png', + 'current_path' => '/../../../../controllers/_files', + ], + 'expectation' => [ + 'name' => 'product_image.png', + 'type' => 'image/png', + 'file' => '/p/r/product_image.png.tmp', + 'url' => 'http://localhost/pub/media/tmp/catalog/product/p/r/product_image.png', + 'tmp_media_path' => '/p/r/product_image.png', + ], + ], + 'upload_image_with_type_gif' => [ + 'file' => [ + 'name' => 'magento_image.gif', + 'type' => 'image/gif', + 'current_path' => '/../../../../_files', + ], + 'expectation' => [ + 'name' => 'magento_image.gif', + 'type' => 'image/gif', + 'file' => '/m/a/magento_image.gif.tmp', + 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.gif', + 'tmp_media_path' => '/m/a/magento_image.gif', + ], + ], + ]; + } + + /** + * Test upload image on admin product page. + * + * @dataProvider uploadActionWithErrorsDataProvider + * @magentoDbIsolation enabled + * @param array $file + * @param array $expectation + * @return void + */ + public function testUploadActionWithErrors(array $file, array $expectation): void + { + if (!empty($file['create_file'])) { + $this->createFileInSysTmpDir($file['name']); + } elseif (!empty($file['copy_file'])) { + $this->copyFileToSysTmpDir($file); + } + + $this->getRequest()->setMethod($this->httpMethod); + $this->dispatch($this->uri); + $jsonBody = $this->serializer->unserialize($this->getResponse()->getBody()); + $this->assertEquals($expectation['message'], $jsonBody['error']); + $this->assertEquals($expectation['errorcode'], $jsonBody['errorcode']); + + if (!empty($expectation['tmp_media_path'])) { + $this->assertFileNotExists( + $this->getFileAbsolutePath($expectation['tmp_media_path']) + ); + } + } + + /** + * @return array + */ + public function uploadActionWithErrorsDataProvider(): array + { + return [ + 'upload_image_with_invalid_type' => [ + 'file' => [ + 'create_file' => true, + 'name' => 'invalid_file.txt', + ], + 'expectation' => [ + 'message' => 'Disallowed file type.', + 'errorcode' => 0, + 'tmp_media_path' => '/i/n/invalid_file.txt', + ], + ], + 'upload_empty_image' => [ + 'file' => [ + 'copy_file' => true, + 'name' => 'magento_empty.jpg', + 'type' => 'image/jpeg', + 'current_path' => '/../../../../_files', + ], + 'expectation' => [ + 'message' => 'Wrong file size.', + 'errorcode' => 0, + 'tmp_media_path' => '/m/a/magento_empty.jpg', + ], + ], + 'upload_without_image' => [ + 'file' => [], + 'expectation' => [ + 'message' => '$_FILES array is empty', + 'errorcode' => 0, + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $_FILES = []; + $this->mediaDirectory->delete('tmp'); + parent::tearDown(); + } + + /** + * Copies file to tmp dir. + * + * @param array $file + * @return void + */ + private function copyFileToSysTmpDir(array $file): void + { + if (!empty($file)) { + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + $fixtureDir = realpath(__DIR__ . $file['current_path']); + $filePath = $tmpDirectory->getAbsolutePath($file['name']); + copy($fixtureDir . DIRECTORY_SEPARATOR . $file['name'], $filePath); + + $_FILES['image'] = [ + 'name' => $file['name'], + 'type' => $file['type'], + 'tmp_name' => $filePath, + ]; + } + } + + /** + * Creates txt file with given name and copies to tmp dir. + * + * @param string $name + * @return void + */ + private function createFileInSysTmpDir(string $name): void + { + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + $filePath = $tmpDirectory->getAbsolutePath($name); + $file = fopen($filePath, "wb"); + fwrite($file, 'some text'); + + $_FILES['image'] = [ + 'name' => $name, + 'type' => 'text/plain', + 'tmp_name' => $filePath, + ]; + } + + /** + * Returns absolute path to file in media tmp dir. + * + * @param string $tmpPath + * @return string + */ + private function getFileAbsolutePath(string $tmpPath): string + { + return $this->mediaDirectory->getAbsolutePath($this->config->getBaseTmpMediaPath() . $tmpPath); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/CreateCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/CreateCustomOptionsTest.php new file mode 100644 index 0000000000000..80f15da647b25 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/CreateCustomOptionsTest.php @@ -0,0 +1,271 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Base test cases for product custom options with type "field". + * Option add via dispatch product controller action save with options data in POST data. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class CreateCustomOptionsTest extends AbstractBackendController +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $optionRepository; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + $this->optionRepository = $this->_objectManager->create(ProductCustomOptionRepositoryInterface::class); + } + + /** + * Test add to product custom option with type "field". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productWithNewOptionsDataProvider + * + * @param array $productPostData + */ + public function testSaveCustomOptionWithTypeField(array $productPostData): void + { + $this->getRequest()->setPostValue($productPostData); + $product = $this->productRepository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->contains('You saved the product.'), + MessageInterface::TYPE_SUCCESS + ); + $productOptions = $this->optionRepository->getProductOptions($product); + $this->assertCount(2, $productOptions); + foreach ($productOptions as $customOption) { + $postOptionData = $productPostData['product']['options'][$customOption->getTitle()] ?? null; + $this->assertNotNull($postOptionData); + $this->assertEquals($postOptionData['title'], $customOption->getTitle()); + $this->assertEquals($postOptionData['type'], $customOption->getType()); + $this->assertEquals($postOptionData['is_require'], $customOption->getIsRequire()); + $this->assertEquals($postOptionData['sku'], $customOption->getSku()); + $this->assertEquals($postOptionData['price'], $customOption->getPrice()); + $this->assertEquals($postOptionData['price_type'], $customOption->getPriceType()); + $maxCharacters = $postOptionData['max_characters'] ?? 0; + $this->assertEquals($maxCharacters, $customOption->getMaxCharacters()); + } + } + + /** + * Return all data for add option to product for all cases. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productWithNewOptionsDataProvider(): array + { + return [ + 'required_options' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + 'not_required_options' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 0, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + 'options_with_fixed_price' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + ], + ], + ], + 'options_with_percent_price' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 20, + 'price_type' => 'percent', + ], + ], + ], + ], + ], + 'options_with_max_charters_configuration' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + 'options_without_max_charters_configuration' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/DeleteCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/DeleteCustomOptionsTest.php new file mode 100644 index 0000000000000..f1af6e6e41cff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/DeleteCustomOptionsTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Base test cases for delete product custom option with type "field". + * Option deleting via product controller action save. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class DeleteCustomOptionsTest extends AbstractBackendController +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $optionRepository; + + /** + * @var ProductCustomOptionInterfaceFactory + */ + private $optionRepositoryFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->optionRepository = $this->_objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->optionRepositoryFactory = $this->_objectManager->get(ProductCustomOptionInterfaceFactory::class); + } + + /** + * Test delete custom option with type "field". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Field::getDataForCreateOptions + * + * @param array $optionData + * @return void + */ + public function testDeleteCustomOptionWithTypeField(array $optionData): void + { + $product = $this->productRepository->get('simple'); + /** @var ProductCustomOptionInterface $option */ + $option = $this->optionRepositoryFactory->create(['data' => $optionData]); + $option->setProductSku($product->getSku()); + $product->setOptions([$option]); + $this->productRepository->save($product); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the product.')]), + MessageInterface::TYPE_SUCCESS + ); + $this->assertCount(0, $this->optionRepository->getProductOptions($product)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/ImagesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/ImagesTest.php new file mode 100644 index 0000000000000..697980d75a715 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/ImagesTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Provide tests for admin product save action with images. + * + * @magentoAppArea adminhtml + */ +class ImagesTest extends AbstractBackendController +{ + /** + * @var Config + */ + private $config; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->config = $this->_objectManager->get(Config::class); + $this->mediaDirectory = $this->_objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + } + + /** + * Test save product with default image. + * + * @dataProvider simpleProductImagesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @magentoDbIsolation enabled + * @param array $postData + * @param array $expectation + * @return void + */ + public function testSaveSimpleProductDefaultImage(array $postData, array $expectation): void + { + $product = $this->productRepository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->equalTo(['You saved the product.']), + MessageInterface::TYPE_SUCCESS + ); + $this->assertSuccessfulImageSave($expectation); + } + + /** + * @return array + */ + public function simpleProductImagesDataProvider(): array + { + return [ + 'simple_product_with_jpg_image' => [ + 'post_data' => [ + 'product' => [ + 'media_gallery' => [ + 'images' => [ + 'lrwuv5ukisn' => [ + 'position' => '1', + 'media_type' => 'image', + 'video_provider' => '', + 'file' => '/m/a//magento_image.jpg.tmp', + 'value_id' => '', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'role' => '', + ], + ], + ], + 'image' => '/m/a//magento_image.jpg.tmp', + 'small_image' => '/m/a//magento_image.jpg.tmp', + 'thumbnail' => '/m/a//magento_image.jpg.tmp', + 'swatch_image' => '/m/a//magento_image.jpg.tmp', + ], + ], + 'expectation' => [ + 'media_gallery_image' => [ + 'position' => '1', + 'media_type' => 'image', + 'file' => '/m/a/magento_image.jpg', + 'label' => '', + 'disabled' => '0', + ], + 'image' => '/m/a/magento_image.jpg', + 'small_image' => '/m/a/magento_image.jpg', + 'thumbnail' => '/m/a/magento_image.jpg', + 'swatch_image' => '/m/a/magento_image.jpg', + ] + ] + ]; + } + + /** + * @param array $expectation + * @return void + */ + private function assertSuccessfulImageSave(array $expectation): void + { + $product = $this->productRepository->get('simple', false, null, true); + $galleryImage = reset($product->getData('media_gallery')['images']); + $expectedGalleryImage = $expectation['media_gallery_image']; + $this->assertEquals($expectedGalleryImage['position'], $galleryImage['position']); + $this->assertEquals($expectedGalleryImage['media_type'], $galleryImage['media_type']); + $this->assertEquals($expectedGalleryImage['label'], $galleryImage['label']); + $this->assertEquals($expectedGalleryImage['disabled'], $galleryImage['disabled']); + $this->assertEquals($expectedGalleryImage['file'], $galleryImage['file']); + $this->assertEquals($expectation['image'], $product->getData('image')); + $this->assertEquals($expectation['small_image'], $product->getData('small_image')); + $this->assertEquals($expectation['thumbnail'], $product->getData('thumbnail')); + $this->assertEquals($expectation['swatch_image'], $product->getData('swatch_image')); + $this->assertFileExists( + $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $expectation['image']) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/LinksTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/LinksTest.php new file mode 100644 index 0000000000000..665d45921d435 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/LinksTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Saving product with linked products + * + * @magentoAppArea adminhtml + */ +class LinksTest extends AbstractBackendController +{ + /** @var array */ + private $linkTypes = [ + 'upsell', + 'crosssell', + 'related', + ]; + + /** @var ProductRepositoryInterface $productRepository */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + } + + /** + * Test add simple related, up-sells, cross-sells product + * + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoDbIsolation enabled + * @return void + */ + public function testAddRelatedUpSellCrossSellProducts(): void + { + $postData = $this->getPostData(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/product/save'); + $this->assertSessionMessages( + $this->equalTo(['You saved the product.']), + MessageInterface::TYPE_SUCCESS + ); + $product = $this->productRepository->get('simple'); + $this->assertEquals( + $this->getExpectedLinks($postData['links']), + $this->getActualLinks($product), + "Expected linked products do not match actual linked products!" + ); + } + + /** + * Get post data for the request + * + * @return array + */ + private function getPostData(): array + { + return [ + 'product' => [ + 'attribute_set_id' => '4', + 'status' => '1', + 'name' => 'Simple Product', + 'sku' => 'simple', + 'url_key' => 'simple-product', + 'type_id' => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ], + 'links' => [ + 'upsell' => [ + ['id' => '10'], + ], + 'crosssell' => [ + ['id' => '11'], + ], + 'related' => [ + ['id' => '12'], + ], + ] + ]; + } + + /** + * Set an array of expected related, up-sells, cross-sells product identifiers + * + * @param array $links + * @return array + */ + private function getExpectedLinks(array $links): array + { + $expectedLinks = []; + foreach ($this->linkTypes as $linkType) { + $expectedLinks[$linkType] = []; + foreach ($links[$linkType] as $productData) { + $expectedLinks[$linkType][] = $productData['id']; + } + } + + return $expectedLinks; + } + + /** + * Get an array of received related, up-sells, cross-sells products + * + * @param ProductInterface|Product $product + * @return array + */ + private function getActualLinks(ProductInterface $product): array + { + $actualLinks = []; + foreach ($this->linkTypes as $linkType) { + $ids = []; + switch ($linkType) { + case 'upsell': + $ids = $product->getUpSellProductIds(); + break; + case 'crosssell': + $ids = $product->getCrossSellProductIds(); + break; + case 'related': + $ids = $product->getRelatedProductIds(); + break; + } + $actualLinks[$linkType] = $ids; + } + + return $actualLinks; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/UpdateCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/UpdateCustomOptionsTest.php new file mode 100644 index 0000000000000..a45c21444a5d7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/UpdateCustomOptionsTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Base test cases for update product custom options with type "field". + * Option updating via dispatch product controller action save with updated options data in POST data. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class UpdateCustomOptionsTest extends AbstractBackendController +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $optionRepository; + + /** + * @var ProductCustomOptionInterfaceFactory + */ + private $optionRepositoryFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->optionRepository = $this->_objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->optionRepositoryFactory = $this->_objectManager->get(ProductCustomOptionInterfaceFactory::class); + } + + /** + * Test add to product custom option with type "field". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Field::getDataForUpdateOptions + * + * @param array $optionData + * @param array $updateData + * @return void + */ + public function testUpdateCustomOptionWithTypeField(array $optionData, array $updateData): void + { + $product = $this->productRepository->get('simple'); + /** @var ProductCustomOptionInterface|Option $option */ + $option = $this->optionRepositoryFactory->create(['data' => $optionData]); + $option->setProductSku($product->getSku()); + $product->setOptions([$option]); + $this->productRepository->save($product); + $currentProductOptions = $this->optionRepository->getProductOptions($product); + $this->assertCount(1, $currentProductOptions); + /** @var ProductCustomOptionInterface $currentOption */ + $currentOption = reset($currentProductOptions); + $postData = [ + 'product' => [ + 'options' => [ + [ + 'option_id' => $currentOption->getOptionId(), + 'product_id' => $product->getId(), + 'type' => $currentOption->getType(), + 'is_require' => $currentOption->getIsRequire(), + 'sku' => $currentOption->getSku(), + 'max_characters' => $currentOption->getMaxCharacters(), + 'title' => $currentOption->getTitle(), + 'sort_order' => $currentOption->getSortOrder(), + 'price' => $currentOption->getPrice(), + 'price_type' => $currentOption->getPriceType(), + 'is_use_default' => false, + ], + ], + ], + ]; + + foreach ($updateData as $methodKey => $newValue) { + $postData = array_replace_recursive( + $postData, + [ + 'product' => [ + 'options' => [ + 0 => [ + $methodKey => $newValue, + ], + ], + ], + ] + ); + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->contains('You saved the product.'), + MessageInterface::TYPE_SUCCESS + ); + $updatedOptions = $this->optionRepository->getProductOptions($product); + $this->assertCount(1, $updatedOptions); + /** @var ProductCustomOptionInterface|Option $updatedOption */ + $updatedOption = reset($updatedOptions); + $this->assertEquals($newValue, $updatedOption->getDataUsingMethod($methodKey)); + $this->assertEquals($option->getOptionId(), $updatedOption->getOptionId()); + $this->assertNotEquals( + $option->getDataUsingMethod($methodKey), + $updatedOption->getDataUsingMethod($methodKey) + ); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/SearchTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/SearchTest.php index 8a33543e93439..cfa8b6022963e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/SearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/SearchTest.php @@ -38,7 +38,8 @@ public function testExecuteNonExistingSearchKey() : void ->setPostValue('limit', 50); $this->dispatch('backend/catalog/product/search'); $responseBody = $this->getResponse()->getBody(); - $this->assertContains('{"options":[],"total":0}', $responseBody); + $jsonResponse = json_decode($responseBody, true); + $this->assertEmpty($jsonResponse['options']); } /** @@ -57,6 +58,24 @@ public function testExecuteNotVisibleIndividuallyProducts() : void ->setPostValue('limit', 50); $this->dispatch('backend/catalog/product/search'); $responseBody = $this->getResponse()->getBody(); - $this->assertContains('{"options":[],"total":0}', $responseBody); + $jsonResponse = json_decode($responseBody, true); + $this->assertEquals(1, $jsonResponse['total']); + $this->assertCount(1, $jsonResponse['options']); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/multiple_mixed_products.php + */ + public function testExecuteEnabledAndDisabledProducts() : void + { + $this->getRequest() + ->setPostValue('searchKey', 'simple') + ->setPostValue('page', 1) + ->setPostValue('limit', 50); + $this->dispatch('backend/catalog/product/search'); + $responseBody = $this->getResponse()->getBody(); + $jsonResponse = json_decode($responseBody, true); + $this->assertEquals(6, $jsonResponse['total']); + $this->assertCount(6, $jsonResponse['options']); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php index 7e034b8b3cb7e..5cb1f862054ba 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php @@ -3,42 +3,146 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -use Magento\Framework\Message\MessageInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Eav\Model\GetAttributeSetByName; +use Magento\TestFramework\TestCase\AbstractBackendController; -class DeleteTest extends \Magento\TestFramework\TestCase\AbstractBackendController +/** + * Test for attribute set deleting. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class DeleteTest extends AbstractBackendController { /** + * @var GetAttributeSetByName + */ + private $getAttributeSetByName; + + /** + * @var ProductInterface|Product + */ + private $product; + + /** + * @var AttributeSetRepositoryInterface + */ + private $attributeSetRepository; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->getAttributeSetByName = $this->_objectManager->get(GetAttributeSetByName::class); + $this->product = $this->_objectManager->get(ProductInterface::class); + $this->attributeSetRepository = $this->_objectManager->get(AttributeSetRepositoryInterface::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Assert that default attribute set is not deleted. + * + * @return void + */ + public function testDefaultAttributeSetIsNotDeleted(): void + { + $productDefaultAttrSetId = (int)$this->product->getDefaultAttributeSetId(); + $this->performDeleteAttributeSetRequest($productDefaultAttrSetId); + $expectedSessionMessage = $this->escaper->escapeHtml((string)__('We can\'t delete this set right now.')); + $this->assertSessionMessages( + $this->equalTo([$expectedSessionMessage]), + MessageInterface::TYPE_ERROR + ); + try { + $this->attributeSetRepository->get($productDefaultAttrSetId); + } catch (NoSuchEntityException $e) { + $this->fail(sprintf('Default attribute set was deleted. Message: %s', $e->getMessage())); + } + } + + /** + * Assert that custom attribute set deleting properly. + * * @magentoDataFixture Magento/Eav/_files/empty_attribute_set.php + * + * @return void */ - public function testDeleteById() + public function testDeleteCustomAttributeSetById(): void { - $attributeSet = $this->getAttributeSetByName('empty_attribute_set'); - $this->getRequest()->setParam('id', $attributeSet->getId())->setMethod(HttpRequest::METHOD_POST); + $this->deleteAttributeSetByNameAndAssert('empty_attribute_set'); + } - $this->dispatch('backend/catalog/product_set/delete/'); + /** + * Assert that product will be deleted if delete attribute set which the product is attached. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_test_attribute_set.php + * + * @return void + */ + public function testProductIsDeletedAfterDeleteItsAttributeSet(): void + { + $this->deleteAttributeSetByNameAndAssert('new_attribute_set'); + $this->expectExceptionObject( + new NoSuchEntityException( + __('The product that was requested doesn\'t exist. Verify the product and try again.') + ) + ); + $this->productRepository->get('simple'); + } - $this->assertNull($this->getAttributeSetByName('empty_attribute_set')); + /** + * Perform request to delete attribute set and assert that attribute set is deleted. + * + * @param string $attributeSetName + * @return void + */ + private function deleteAttributeSetByNameAndAssert(string $attributeSetName): void + { + $attributeSet = $this->getAttributeSetByName->execute($attributeSetName); + $this->performDeleteAttributeSetRequest((int)$attributeSet->getAttributeSetId()); $this->assertSessionMessages( - $this->equalTo(['The attribute set has been removed.']), + $this->equalTo([(string)__('The attribute set has been removed.')]), MessageInterface::TYPE_SUCCESS ); - $this->assertRedirect($this->stringContains('catalog/product_set/index/')); + $this->assertNull($this->getAttributeSetByName->execute($attributeSetName)); } /** - * Retrieve attribute set based on given name. + * Perform "catalog/product_set/delete" controller dispatch. * - * @param string $attributeSetName - * @return \Magento\Eav\Model\Entity\Attribute\Set|null + * @param int $attributeSetId + * @return void */ - protected function getAttributeSetByName($attributeSetName) + private function performDeleteAttributeSetRequest(int $attributeSetId): void { - $attributeSet = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Eav\Model\Entity\Attribute\Set::class - )->load($attributeSetName, 'attribute_set_name'); - return $attributeSet->getId() === null ? null : $attributeSet; + $this->getRequest() + ->setParam('id', $attributeSetId) + ->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product_set/delete/'); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php index 8ccd426424a29..1edd494dabbe3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php @@ -3,64 +3,363 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Repository; +use Magento\Developer\Model\Logger\Handler\Syslog; +use Magento\Eav\Api\AttributeManagementInterface; use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Eav\Api\Data\AttributeSetInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Logger\Handler\System; +use Magento\Framework\Logger\Monolog; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; -class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController +/** + * Testing for saving an existing or creating a new attribute set. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @magentoAppArea adminhtml + */ +class SaveTest extends AbstractBackendController { /** + * @var string + */ + private $systemLogPath = ''; + + /** + * @var Monolog + */ + private $logger; + + /** + * @var Syslog + */ + private $syslogHandler; + + /** + * @var AttributeManagementInterface + */ + private $attributeManagement; + + /** + * @var DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var Repository + */ + private $attributeRepository; + + /** + * @var AttributeSetRepositoryInterface + */ + private $attributeSetRepository; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @var Json + */ + private $json; + + /** + * @inheritDoc + */ + public function setUp() + { + parent::setUp(); + $this->logger = $this->_objectManager->get(Monolog::class); + $this->syslogHandler = $this->_objectManager->create( + Syslog::class, + [ + 'filePath' => Bootstrap::getInstance()->getAppTempDir(), + ] + ); + $this->attributeManagement = $this->_objectManager->get(AttributeManagementInterface::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->attributeRepository = $this->_objectManager->get(Repository::class); + $this->dataObjectHelper = $this->_objectManager->get(DataObjectHelper::class); + $this->attributeSetRepository = $this->_objectManager->get(AttributeSetRepositoryInterface::class); + $this->eavConfig = $this->_objectManager->get(Config::class); + $this->json = $this->_objectManager->get(Json::class); + } + + /** + * @inheritdoc + */ + public function tearDown() + { + $this->attributeRepository->get('country_of_manufacture')->setIsUserDefined(false); + parent::tearDown(); + } + + /** + * Test that new attribute set based on default attribute set will be successfully created. + * + * @magentoDbIsolation enabled + * + * @return void + */ + public function testCreateNewAttributeSetBasedOnDefaultAttributeSet(): void + { + $this->createAttributeSetBySkeletonAndAssert( + 'Attribute set name for test', + $this->getCatalogProductDefaultAttributeSetId() + ); + } + + /** + * Test that new attribute set based on custom attribute set will be successfully created. + * + * @magentoDataFixture Magento/Catalog/_files/attribute_set_with_renamed_group.php + * + * @magentoDbIsolation enabled + * + * @return void + */ + public function testCreateNewAttributeSetBasedOnCustomAttributeSet(): void + { + $existCustomAttributeSet = $this->getAttributeSetByName('attribute_set_test'); + $this->createAttributeSetBySkeletonAndAssert( + 'Attribute set name for test', + (int)$existCustomAttributeSet->getAttributeSetId() + ); + } + + /** + * Test that new attribute set based on custom attribute set will be successfully created. + * + * @magentoDbIsolation enabled + * + * @return void + */ + public function testGotErrorDuringCreateAttributeSetWithoutName(): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue( + [ + 'gotoEdit' => '1', + 'skeleton_set' => $this->getCatalogProductDefaultAttributeSetId(), + ] + ); + $this->dispatch('backend/catalog/product_set/save/'); + $this->assertSessionMessages( + $this->equalTo([(string)__('The attribute set name is empty. Enter the name and try again.')]), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Test that exception throws during save attribute set name process if name of attribute set already exists. + * * @magentoDataFixture Magento/Catalog/_files/attribute_set_with_renamed_group.php + * @return void */ - public function testAlreadyExistsExceptionProcessingWhenGroupCodeIsDuplicated() + public function testAlreadyExistsExceptionProcessingWhenGroupCodeIsDuplicated(): void { $attributeSet = $this->getAttributeSetByName('attribute_set_test'); $this->assertNotEmpty($attributeSet, 'Attribute set with name "attribute_set_test" is missed'); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); - $this->getRequest()->setPostValue('data', json_encode([ - 'attribute_set_name' => 'attribute_set_test', - 'groups' => [ - ['ynode-418', 'attribute-group-name', 1], - ], - 'attributes' => [ - ['9999', 'ynode-418', 1, null] - ], - 'not_attributes' => [], - 'removeGroups' => [], - ])); + $this->getRequest()->setPostValue( + 'data', + $this->json->serialize( + [ + 'attribute_set_name' => 'attribute_set_test', + 'groups' => [ + ['ynode-418', 'attribute-group-name', 1], + ], + 'attributes' => [ + ['9999', 'ynode-418', 1, null] + ], + 'not_attributes' => [], + 'removeGroups' => [], + ] + ) + ); $this->dispatch('backend/catalog/product_set/save/id/' . $attributeSet->getAttributeSetId()); - $jsonResponse = json_decode($this->getResponse()->getBody()); + $jsonResponse = $this->json->unserialize($this->getResponse()->getBody()); $this->assertNotNull($jsonResponse); - $this->assertEquals(1, $jsonResponse->error); + $this->assertEquals(1, $jsonResponse['error']); $this->assertContains( - 'Attribute group with same code already exist. Please rename "attribute-group-name" group', - $jsonResponse->message + (string)__('Attribute group with same code already exist. Please rename "attribute-group-name" group'), + $jsonResponse['message'] ); } /** + * Test behavior when attribute set was changed to a new set + * with deleted attribute from the previous set. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/attribute_set_based_on_default.php + * @magentoDbIsolation disabled + * @return void + */ + public function testRemoveAttributeFromAttributeSet(): void + { + $message = 'Attempt to load value of nonexistent EAV attribute'; + $this->removeSyslog(); + $attributeSet = $this->getAttributeSetByName('new_attribute_set'); + $product = $this->productRepository->get('simple'); + $this->attributeRepository->get('country_of_manufacture')->setIsUserDefined(true); + $this->attributeManagement->unassign($attributeSet->getId(), 'country_of_manufacture'); + $productData = [ + 'country_of_manufacture' => 'Angola' + ]; + $this->dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class); + $this->productRepository->save($product); + $product->setAttributeSetId($attributeSet->getId()); + $product = $this->productRepository->save($product); + $this->dispatch('backend/catalog/product/edit/id/' . $product->getEntityId()); + $syslogPath = $this->getSyslogPath(); + $syslogContent = file_exists($syslogPath) ? file_get_contents($syslogPath) : ''; + $this->assertNotContains($message, $syslogContent); + } + + /** + * Retrieve system.log file path. + * + * @return string + */ + private function getSyslogPath(): string + { + if (!$this->systemLogPath) { + foreach ($this->logger->getHandlers() as $handler) { + if ($handler instanceof System) { + $this->systemLogPath = $handler->getUrl(); + } + } + } + + return $this->systemLogPath; + } + + /** + * Remove system.log file + * + * @return void + */ + private function removeSyslog(): void + { + $this->syslogHandler->close(); + if (file_exists($this->getSyslogPath())) { + unlink($this->getSyslogPath()); + } + } + + /** + * Search and return attribute set by name. + * * @param string $attributeSetName * @return AttributeSetInterface|null */ - protected function getAttributeSetByName($attributeSetName) + private function getAttributeSetByName(string $attributeSetName): ?AttributeSetInterface { - $objectManager = Bootstrap::getObjectManager(); - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); $searchCriteriaBuilder->addFilter('attribute_set_name', $attributeSetName); - - /** @var AttributeSetRepositoryInterface $attributeSetRepository */ - $attributeSetRepository = $objectManager->get(AttributeSetRepositoryInterface::class); - $result = $attributeSetRepository->getList($searchCriteriaBuilder->create()); + $result = $this->attributeSetRepository->getList($searchCriteriaBuilder->create()); $items = $result->getItems(); - return $result->getTotalCount() ? array_pop($items) : null; + + return array_pop($items); + } + + /** + * Create attribute set by skeleton attribute set id and assert that attribute set + * created successfully and attributes from skeleton attribute set and created attribute set are equals. + * + * @param string $attributeSetName + * @param int $skeletonAttributeSetId + * @return void + */ + private function createAttributeSetBySkeletonAndAssert( + string $attributeSetName, + int $skeletonAttributeSetId + ): void { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue( + [ + 'attribute_set_name' => $attributeSetName, + 'gotoEdit' => '1', + 'skeleton_set' => $skeletonAttributeSetId, + ] + ); + $this->dispatch('backend/catalog/product_set/save/'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the attribute set.')]), + MessageInterface::TYPE_SUCCESS + ); + $createdAttributeSet = $this->getAttributeSetByName($attributeSetName); + $existAttributeSet = $this->attributeSetRepository->get($skeletonAttributeSetId); + + $this->assertNotNull($createdAttributeSet); + $this->assertEquals($attributeSetName, $createdAttributeSet->getAttributeSetName()); + + $this->assertAttributeSetsAttributesAreEquals($createdAttributeSet, $existAttributeSet); + } + + /** + * Assert that both attribute sets contains identical attributes by attribute ids. + * + * @param AttributeSetInterface $createdAttributeSet + * @param AttributeSetInterface $existAttributeSet + * @return void + */ + private function assertAttributeSetsAttributesAreEquals( + AttributeSetInterface $createdAttributeSet, + AttributeSetInterface $existAttributeSet + ): void { + $expectedAttributeIds = array_keys( + $this->attributeManagement->getAttributes( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $existAttributeSet->getAttributeSetId() + ) + ); + sort($expectedAttributeIds); + $actualAttributeIds = array_keys( + $this->attributeManagement->getAttributes( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $createdAttributeSet->getAttributeSetId() + ) + ); + sort($actualAttributeIds); + $this->assertSame($expectedAttributeIds, $actualAttributeIds); + } + + /** + * Retrieve default catalog product attribute set ID. + * + * @return int + */ + private function getCatalogProductDefaultAttributeSetId(): int + { + return (int)$this->eavConfig + ->getEntityType(ProductAttributeInterface::ENTITY_TYPE_CODE) + ->getDefaultAttributeSetId(); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/UpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/UpdateTest.php new file mode 100644 index 0000000000000..765f59b15be83 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/UpdateTest.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Set; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Api\AttributeManagementInterface; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Eav\Api\Data\AttributeGroupInterface; +use Magento\Eav\Api\Data\AttributeSetInterface; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\Eav\Model\GetAttributeSetByName; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test update attribute set. + */ +class UpdateTest extends AbstractBackendController +{ + /** + * @var Json + */ + private $json; + + /** + * @var AttributeSetRepositoryInterface + */ + private $attributeSetRepository; + + /** + * @var AttributeManagementInterface + */ + private $attributeManagement; + + /** + * @var CollectionFactory + */ + private $attributeGroupCollectionFactory; + + /** + * @var GetAttributeSetByName + */ + private $getAttributeSetByName; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->json = $this->_objectManager->get(Json::class); + $this->attributeSetRepository = $this->_objectManager->get(AttributeSetRepositoryInterface::class); + $this->attributeManagement = $this->_objectManager->get(AttributeManagementInterface::class); + $this->attributeGroupCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + $this->getAttributeSetByName = $this->_objectManager->get(GetAttributeSetByName::class); + } + + /** + * Test that name of attribute set will update/change correctly. + * + * @magentoDataFixture Magento/Catalog/_files/attribute_set_based_on_default.php + * + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateAttributeSetName(): void + { + $attributeSet = $this->getAttributeSetByName->execute('new_attribute_set'); + $currentAttrSetName = $attributeSet->getAttributeSetName(); + $this->assertNotNull($attributeSet); + $postData = $this->prepareDataToRequest($attributeSet); + $updateName = 'New attribute set name'; + $postData['attribute_set_name'] = $updateName; + $this->performRequest((int)$attributeSet->getAttributeSetId(), $postData); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the attribute set.')]), + MessageInterface::TYPE_SUCCESS + ); + $updatedAttributeSet = $this->attributeSetRepository->get((int)$attributeSet->getAttributeSetId()); + $this->assertEquals($updateName, $updatedAttributeSet->getAttributeSetName()); + $updatedAttributeSet->setAttributeSetName($currentAttrSetName); + $this->attributeSetRepository->save($updatedAttributeSet); + } + + /** + * Test add new group to custom attribute set. + * + * @magentoDataFixture Magento/Catalog/_files/attribute_set_based_on_default.php + * + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateAttributeSetWithNewGroup(): void + { + $currentAttrSet = $this->getAttributeSetByName->execute('new_attribute_set'); + $this->assertNotNull($currentAttrSet); + $attrSetId = (int)$currentAttrSet->getAttributeSetId(); + $currentAttrGroups = $this->getAttributeSetGroupCollection($attrSetId)->getItems(); + $newGroupName = 'Test attribute group name'; + $newGroupSortOrder = 11; + $postData = $this->prepareDataToRequest($currentAttrSet); + $postData['groups'][] = [ + null, + $newGroupName, + $newGroupSortOrder, + ]; + $this->performRequest($attrSetId, $postData); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the attribute set.')]), + MessageInterface::TYPE_SUCCESS + ); + $updatedAttrGroups = $this->getAttributeSetGroupCollection($attrSetId)->getItems(); + $diffGroups = array_diff_key($updatedAttrGroups, $currentAttrGroups); + $this->assertCount(1, $diffGroups); + /** @var AttributeGroupInterface $newGroup */ + $newGroup = reset($diffGroups); + $this->assertEquals($newGroupName, $newGroup->getAttributeGroupName()); + $this->assertEquals($newGroupSortOrder, $newGroup->getSortOrder()); + } + + /** + * Test delete custom group from custom attribute set. + * + * @magentoDataFixture Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group.php + * + * @magentoDbIsolation disabled + * + * @return void + */ + public function testDeleteCustomGroupFromCustomAttributeSet(): void + { + $testGroupName = 'Test attribute group name'; + $currentAttrSet = $this->getAttributeSetByName->execute('new_attribute_set'); + $this->assertNotNull($currentAttrSet); + $attrSetId = (int)$currentAttrSet->getAttributeSetId(); + $currentAttrGroupsCollection = $this->getAttributeSetGroupCollection($attrSetId); + $customGroup = $currentAttrGroupsCollection->getItemByColumnValue( + AttributeGroupInterface::GROUP_NAME, + $testGroupName + ); + $this->assertNotNull($customGroup); + $postData = $this->prepareDataToRequest($currentAttrSet); + $postData['removeGroups'] = [ + $customGroup->getAttributeGroupId() + ]; + $this->performRequest($attrSetId, $postData); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the attribute set.')]), + MessageInterface::TYPE_SUCCESS + ); + $updatedAttrGroups = $this->getAttributeSetGroupCollection($attrSetId)->getItems(); + $diffGroups = array_diff_key($currentAttrGroupsCollection->getItems(), $updatedAttrGroups); + $this->assertCount(1, $diffGroups); + /** @var AttributeGroupInterface $deletedGroup */ + $deletedGroup = reset($diffGroups); + $this->assertEquals($testGroupName, $deletedGroup->getAttributeGroupName()); + } + + /** + * Process attribute set save request. + * + * @param int $attributeSetId + * @param array $postData + * @return void + */ + private function performRequest(int $attributeSetId, array $postData = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue( + 'data', + $this->json->serialize($postData) + ); + $this->dispatch('backend/catalog/product_set/save/id/' . $attributeSetId); + } + + /** + * Prepare default data to request from attribute set. + * + * @param AttributeSetInterface $attributeSet + * @return array + */ + private function prepareDataToRequest(AttributeSetInterface $attributeSet): array + { + $result = [ + 'attribute_set_name' => $attributeSet->getAttributeSetName(), + 'removeGroups' => [], + 'not_attributes' => [], + ]; + $groups = $attributes = []; + /** @var AttributeGroupInterface $group */ + foreach ($this->getAttributeSetGroupCollection((int)$attributeSet->getAttributeSetId()) as $group) { + $groups[] = [ + $group->getAttributeGroupId(), + $group->getAttributeGroupName(), + $group->getSortOrder(), + ]; + } + $attributeSetAttributes = $this->attributeManagement->getAttributes( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSet->getAttributeSetId() + ); + foreach ($attributeSetAttributes as $attribute) { + $attributes[] = [ + $attribute->getAttributeId(), + $attribute->getAttributeGroupId(), + $attribute->getSortOrder(), + ]; + } + $result['groups'] = $groups; + $result['attributes'] = $attributes; + + return $result; + } + + /** + * Build attribute set groups collection by attribute set id. + * + * @param int $attributeSetId + * @return Collection + */ + private function getAttributeSetGroupCollection(int $attributeSetId): Collection + { + $groupCollection = $this->attributeGroupCollectionFactory->create(); + $groupCollection->setAttributeSetFilter($attributeSetId); + + return $groupCollection; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index acec996d0c406..7ca04863f58a1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -3,19 +3,59 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate; +use Magento\Framework\Acl\Builder; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Message\Manager; use Magento\Framework\App\Request\Http as HttpRequest; -use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ProductRepositoryFactory; use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\CacheCleaner; /** + * Test class for Product adminhtml actions + * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var Builder + */ + private $aclBuilder; + + /** + * @var ProductRepositoryFactory + */ + private $repositoryFactory; + + /** + * @var ProductResource + */ + private $resourceModel; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); + $this->repositoryFactory = Bootstrap::getObjectManager()->get(ProductRepositoryFactory::class); + $this->resourceModel = Bootstrap::getObjectManager()->get(ProductResource::class); + } + /** * Test calling save with invalid product's ID. */ @@ -39,7 +79,8 @@ public function testSaveActionWithDangerRequest() public function testSaveActionAndNew() { $this->getRequest()->setPostValue(['back' => 'new']); - $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + /** @var ProductRepository $repository */ + $repository = $this->repositoryFactory->create(); $product = $repository->get('simple'); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); @@ -59,10 +100,10 @@ public function testSaveActionAndNew() public function testSaveActionAndDuplicate() { $this->getRequest()->setPostValue(['back' => 'duplicate']); - $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + /** @var ProductRepository $repository */ + $repository = $this->repositoryFactory->create(); $product = $repository->get('simple'); - $this->getRequest()->setMethod(HttpRequest::METHOD_POST); - $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSaveAndDuplicateAction($product); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/edit/')); $this->assertRedirect( $this->logicalNot( @@ -71,14 +112,31 @@ public function testSaveActionAndDuplicate() ) ) ); - $this->assertSessionMessages( - $this->contains('You saved the product.'), - MessageInterface::TYPE_SUCCESS - ); - $this->assertSessionMessages( - $this->contains('You duplicated the product.'), - MessageInterface::TYPE_SUCCESS - ); + } + + /** + * Tests of saving and duplicating existing product after the script execution. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSaveActionAndDuplicateWithUrlPathAttribute() + { + /** @var ProductRepository $repository */ + $repository = $this->repositoryFactory->create(); + /** @var Product $product */ + $product = $repository->get('simple'); + + // set url_path attribute and check it + $product->setData('url_path', $product->getSku()); + $repository->save($product); + $urlPathAttribute = $product->getCustomAttribute('url_path'); + $this->assertEquals($urlPathAttribute->getValue(), $product->getSku()); + + // clean cache + CacheCleaner::cleanAll(); + + // dispatch Save&Duplicate action and check it + $this->assertSaveAndDuplicateAction($product); } /** @@ -130,7 +188,8 @@ public function testIndexAction() */ public function testEditAction() { - $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + /** @var ProductRepository $repository */ + $repository = $this->repositoryFactory->create(); $product = $repository->get('simple'); $this->dispatch('backend/catalog/product/edit/id/' . $product->getEntityId()); $body = $this->getResponse()->getBody(); @@ -349,10 +408,198 @@ public function saveActionTierPriceDataProvider() */ private function getProductData(array $tierPrice) { - $productRepositoryInterface = $this->_objectManager->get(ProductRepositoryInterface::class); - $product = $productRepositoryInterface->get('tier_prices')->getData(); + /** @var ProductRepository $repo */ + $repo = $this->repositoryFactory->create(); + $product = $repo->get('tier_prices')->getData(); $product['tier_price'] = $tierPrice; unset($product['entity_id']); return $product; } + + /** + * Check whether additional authorization is required for the design fields. + * + * @magentoDbIsolation enabled + * @throws \Throwable + * @return void + */ + public function testSaveDesign(): void + { + $requestData = [ + 'product' => [ + 'type' => 'simple', + 'sku' => 'simple', + 'store' => '0', + 'set' => '4', + 'back' => 'edit', + 'type_id' => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE, + 'product' => [], + 'is_downloadable' => '0', + 'affect_configurable_product_attributes' => '1', + 'new_variation_attribute_set_id' => '4', + 'use_default' => [ + 'gift_message_available' => '0', + 'gift_wrapping_available' => '0' + ], + 'configurable_matrix_serialized' => '[]', + 'associated_product_ids_serialized' => '[]' + ] + ]; + $uri = 'backend/catalog/product/save'; + + //Trying to update product's design settings without proper permissions. + //Expected list of sessions messages collected throughout the controller calls. + $sessionMessages = ['Not allowed to edit the product\'s design attributes']; + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_product_design'); + $requestData['product']['custom_design'] = '1'; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->dispatch($uri); + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + + //Trying again with the permissions. + $this->aclBuilder->getAcl()->allow(null, ['Magento_Catalog::products', 'Magento_Catalog::edit_product_design']); + $this->getRequest()->setDispatched(false); + $this->dispatch($uri); + /** @var ProductRepository $repo */ + $repo = $this->repositoryFactory->create(); + $product = $repo->get('simple'); + $this->assertNotEmpty($product->getCustomDesign()); + $this->assertEquals(1, $product->getCustomDesign()); + //No new error messages + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Save design without the permissions but with default values. + * + * @magentoDbIsolation enabled + * @throws \Throwable + * @return void + */ + public function testSaveDesignWithDefaults(): void + { + $optionsContainerDefault = $this->resourceModel->getAttribute('options_container')->getDefaultValue(); + $requestData = [ + 'product' => [ + 'type' => 'simple', + 'sku' => 'simple', + 'store' => '0', + 'set' => '4', + 'back' => 'edit', + 'product' => [], + 'type_id' => \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE, + 'is_downloadable' => '0', + 'affect_configurable_product_attributes' => '1', + 'new_variation_attribute_set_id' => '4', + 'use_default' => [ + 'gift_message_available' => '0', + 'gift_wrapping_available' => '0' + ], + 'configurable_matrix_serialized' => '[]', + 'associated_product_ids_serialized' => '[]', + 'options_container' => $optionsContainerDefault + ] + ]; + $uri = 'backend/catalog/product/save'; + + //Updating product's design settings without proper permissions. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_product_design'); + //Testing that special "No Update" value is treated as no change. + $requestData['product']['custom_layout_update_file'] = LayoutUpdate::VALUE_NO_UPDATE; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->dispatch($uri); + + //Validating saved entity. + /** @var ProductRepository $repo */ + $repo = $this->repositoryFactory->create(); + $product = $repo->get('simple'); + $this->assertNotNull($product->getData('options_container')); + $this->assertEquals($optionsContainerDefault, $product->getData('options_container')); + } + + /** + * Test custom update files functionality. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled + * @throws \Throwable + * @return void + */ + public function testSaveCustomLayout(): void + { + $file = 'test_file'; + /** @var ProductRepository $repo */ + $repo = $this->repositoryFactory->create(); + $product = $repo->get('simple'); + /** @var ProductLayoutUpdateManager $layoutManager */ + $layoutManager = Bootstrap::getObjectManager()->get(ProductLayoutUpdateManager::class); + $layoutManager->setFakeFiles((int)$product->getId(), [$file]); + $productData = $product->getData(); + unset($productData['options']); + unset($productData[$product->getIdFieldName()]); + $requestData = [ + 'product' => $productData + ]; + $uri = 'backend/catalog/product/save'; + + //Saving a wrong file + $requestData['product']['custom_layout_update_file'] = $file . 'INVALID'; + $this->getRequest()->setDispatched(false); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('id', $product->getId()); + $this->dispatch($uri); + $this->assertSessionMessages( + self::equalTo(['Selected layout update is not available']), + MessageInterface::TYPE_ERROR + ); + + //Checking that the value is not saved + /** @var ProductRepository $repo */ + $repo = $this->repositoryFactory->create(); + $product = $repo->get('simple'); + $this->assertEmpty($product->getData('custom_layout_update_file')); + + //Saving the correct file + $requestData['product']['custom_layout_update_file'] = $file; + $this->getRequest()->setDispatched(false); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setParam('id', $product->getId()); + $this->dispatch($uri); + + //Checking that the value is saved + /** @var ProductRepository $repo */ + $repo = $this->repositoryFactory->create(); + $product = $repo->get('simple'); + $this->assertEquals($file, $product->getData('custom_layout_update_file')); + } + + /** + * Dispatch Save&Duplicate action and check it + * + * @param Product $product + */ + private function assertSaveAndDuplicateAction(Product $product) + { + $this->getRequest()->setPostValue(['back' => 'duplicate']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->contains('You saved the product.'), + MessageInterface::TYPE_SUCCESS + ); + $this->assertSessionMessages( + $this->contains('You duplicated the product.'), + MessageInterface::TYPE_SUCCESS + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php new file mode 100644 index 0000000000000..1b51c65e1e853 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Category; + +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Response\Http; +use Magento\Framework\Registry; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Checks category availability on storefront by url rewrite + * + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @magentoDbIsolation enabled + */ +class CategoryUrlRewriteTest extends AbstractController +{ + /** @var Registry */ + private $registry; + + /** @var ScopeConfigInterface */ + private $config; + + /** @var string */ + private $categoryUrlSuffix; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->config = $this->_objectManager->get(ScopeConfigInterface::class); + $this->registry = $this->_objectManager->get(Registry::class); + $this->categoryUrlSuffix = $this->config->getValue( + CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + * @dataProvider categoryRewriteProvider + * @param int $categoryId + * @param string $urlPath + * @return void + */ + public function testCategoryUrlRewrite(int $categoryId, string $urlPath): void + { + $this->dispatch(sprintf($urlPath, $this->categoryUrlSuffix)); + $currentCategory = $this->registry->registry('current_category'); + $response = $this->getResponse(); + $this->assertEquals( + Http::STATUS_CODE_200, + $response->getHttpResponseCode(), + 'Response code does not match expected value' + ); + $this->assertNotNull($currentCategory); + $this->assertEquals($categoryId, $currentCategory->getId()); + } + + /** + * @return array + */ + public function categoryRewriteProvider(): array + { + return [ + [ + 'category_id' => 400, + 'url_path' => '/category-1%s', + ], + [ + 'category_id' => 401, + 'url_path' => '/category-1/category-1-1%s', + ], + [ + 'category_id' => 402, + 'url_path' => '/category-1/category-1-1/category-1-1-1%s', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php index 87b8d4a117e2d..c18a867a9b76e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php @@ -3,24 +3,75 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\TestCase\AbstractController; + /** - * Test class for \Magento\Catalog\Controller\Category. + * Responsible for testing category view action on strorefront. * + * @see \Magento\Catalog\Controller\Category\View * @magentoAppArea frontend */ -class CategoryTest extends \Magento\TestFramework\TestCase\AbstractController +class CategoryTest extends AbstractController { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Registry + */ + private $registry; + + /** + * @var Session + */ + private $session; + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->session = $this->objectManager->get(Session::class); + } + + /** + * @inheritdoc + */ public function assert404NotFound() { parent::assert404NotFound(); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->assertNull($objectManager->get(\Magento\Framework\Registry::class)->registry('current_category')); + + $this->assertNull($this->registry->registry('current_category')); } - public function getViewActionDataProvider() + /** + * @return array + */ + public function getViewActionDataProvider(): array { return [ 'category without children' => [ @@ -56,51 +107,96 @@ public function getViewActionDataProvider() * @dataProvider getViewActionDataProvider * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_product_ids.php * @magentoDbIsolation disabled + * @param int $categoryId + * @param array $expectedHandles + * @param array $expectedContent + * @return void */ - public function testViewAction($categoryId, array $expectedHandles, array $expectedContent) + public function testViewAction(int $categoryId, array $expectedHandles, array $expectedContent): void { $this->dispatch("catalog/category/view/id/{$categoryId}"); - - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var $currentCategory \Magento\Catalog\Model\Category */ - $currentCategory = $objectManager->get(\Magento\Framework\Registry::class)->registry('current_category'); - $this->assertInstanceOf(\Magento\Catalog\Model\Category::class, $currentCategory); + /** @var $currentCategory Category */ + $currentCategory = $this->registry->registry('current_category'); + $this->assertInstanceOf(Category::class, $currentCategory); $this->assertEquals($categoryId, $currentCategory->getId(), 'Category in registry.'); - $lastCategoryId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Session::class - )->getLastVisitedCategoryId(); + $lastCategoryId = $this->session->getLastVisitedCategoryId(); $this->assertEquals($categoryId, $lastCategoryId, 'Last visited category.'); /* Layout updates */ - $handles = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - )->getUpdate()->getHandles(); + $handles = $this->layout->getUpdate()->getHandles(); foreach ($expectedHandles as $expectedHandleName) { $this->assertContains($expectedHandleName, $handles); } $responseBody = $this->getResponse()->getBody(); - /* Response content */ foreach ($expectedContent as $expectedText) { $this->assertStringMatchesFormat($expectedText, $responseBody); } } - public function testViewActionNoCategoryId() + /** + * @return void + */ + public function testViewActionNoCategoryId(): void { $this->dispatch('catalog/category/view/'); $this->assert404NotFound(); } - public function testViewActionInactiveCategory() + /** + * @return void + */ + public function testViewActionNotExistingCategory(): void { $this->dispatch('catalog/category/view/id/8'); $this->assert404NotFound(); } + + /** + * Checks that disabled category is not available in storefront + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/inactive_category.php + * @return void + */ + public function testViewActionDisabledCategory(): void + { + $this->dispatch('catalog/category/view/id/111'); + + $this->assert404NotFound(); + } + + /** + * Check that custom layout update files is employed. + * + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_product_ids.php + * @return void + */ + public function testViewWithCustomUpdate(): void + { + //Setting a fake file for the category. + $file = 'test-file'; + $categoryId = 5; + /** @var CategoryLayoutUpdateManager $layoutManager */ + $layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); + $layoutManager->setCategoryFakeFiles($categoryId, [$file]); + /** @var CategoryRepositoryInterface $categoryRepo */ + $categoryRepo = Bootstrap::getObjectManager()->create(CategoryRepositoryInterface::class); + $category = $categoryRepo->get($categoryId); + //Updating the custom attribute. + $category->setCustomAttribute('custom_layout_update_file', $file); + $categoryRepo->save($category); + + //Viewing the category + $this->dispatch("catalog/category/view/id/$categoryId"); + //Layout handles must contain the file. + $handles = Bootstrap::getObjectManager()->get(\Magento\Framework\View\LayoutInterface::class) + ->getUpdate() + ->getHandles(); + $this->assertContains("catalog_category_view_selectable_{$categoryId}_{$file}", $handles); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ProductUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ProductUrlRewriteTest.php new file mode 100644 index 0000000000000..d55c85d9b9d00 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ProductUrlRewriteTest.php @@ -0,0 +1,206 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Product; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Registry; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\Response; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Checks product availability on storefront by url rewrite + * + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @magentoDbIsolation enabled + */ +class ProductUrlRewriteTest extends AbstractController +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Registry */ + private $registry; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var string */ + private $urlSuffix; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->config = $this->_objectManager->get(ScopeConfigInterface::class); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + $this->registry = $this->_objectManager->get(Registry::class); + $this->categoryRepository = $this->_objectManager->create(CategoryRepositoryInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->urlSuffix = $this->config->getValue( + ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testProductUrlRewrite(): void + { + $product = $this->productRepository->get('simple2'); + $url = $this->prepareUrl($product->getUrlKey()); + $this->dispatch($url); + + $this->assertProductIsVisible($product); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @return void + */ + public function testCategoryProductUrlRewrite(): void + { + $category = $this->categoryRepository->get(333); + $product = $this->productRepository->get('simple333'); + $url = $this->prepareUrl($category->getUrlKey(), false) . $this->prepareUrl($product->getUrlKey()); + $this->dispatch($url); + + $this->assertProductIsVisible($product); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testProductRedirect(): void + { + $product = $this->productRepository->get('simple2'); + $oldUrl = $this->prepareUrl($product->getUrlKey()); + $data = [ + 'url_key' => 'new-url-key', + 'url_key_create_redirect' => $product->getUrlKey(), + 'save_rewrites_history' => true, + ]; + $this->updateProduct($product, $data); + $this->dispatch($oldUrl); + + $this->assertRedirect($this->stringContains($this->prepareUrl('new-url-key'))); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testMultistoreProductUrlRewrite(): void + { + $currentStore = $this->storeManager->getStore(); + $product = $this->productRepository->get('simple2'); + $firstStoreUrl = $this->prepareUrl($product->getUrlKey()); + $secondStoreId = $this->storeManager->getStore('fixturestore')->getId(); + $this->storeManager->setCurrentStore($secondStoreId); + + try { + $product = $this->updateProduct($product, ['url_key' => 'second-store-url-key']); + $this->assertEquals('second-store-url-key', $product->getUrlKey()); + $secondStoreUrl = $this->prepareUrl($product->getUrlKey()); + + $this->dispatch($secondStoreUrl); + $this->assertProductIsVisible($product); + $this->cleanUpCachedObjects(); + } finally { + $this->storeManager->setCurrentStore($currentStore); + } + + $this->dispatch($firstStoreUrl); + $this->assertProductIsVisible($product); + } + + /** + * Update product + * + * @param ProductInterface $product + * @param array $data + * @return ProductInterface + */ + private function updateProduct(ProductInterface $product, array $data): ProductInterface + { + $product->addData($data); + + return $this->productRepository->save($product); + } + + /** + * Clean up cached objects + * + * @return void + */ + private function cleanUpCachedObjects(): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_objectManager->removeSharedInstance(Response::class); + $this->_response = null; + $this->_request = null; + } + + /** + * Prepare url to dispatch + * + * @param string $urlKey + * @param bool $addSuffix + * @return string + */ + private function prepareUrl(string $urlKey, bool $addSuffix = true): string + { + $url = $addSuffix ? '/' . $urlKey . $this->urlSuffix : '/' . $urlKey; + + return $url; + } + + /** + * Assert that product is available in storefront + * + * @param ProductInterface $product + * @return void + */ + private function assertProductIsVisible(ProductInterface $product): void + { + $this->assertEquals( + Response::STATUS_CODE_200, + $this->getResponse()->getHttpResponseCode(), + 'Wrong response code is returned' + ); + $currentProduct = $this->registry->registry('current_product'); + $this->assertNotNull($currentProduct); + $this->assertEquals( + $product->getSku(), + $currentProduct->getSku(), + 'Wrong product is registered' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php index 92a782deee65a..f45c9934acfc1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php @@ -3,18 +3,91 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Product; +use Magento\Catalog\Api\AttributeSetRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Eav\Model\Entity\Type; +use Magento\Framework\App\Http; +use Magento\Framework\Registry; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Eav\Model\GetAttributeSetByName; +use Magento\TestFramework\Request; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Framework\Logger\Monolog as MagentoMonologLogger; +use Magento\TestFramework\Response; +use Magento\TestFramework\TestCase\AbstractController; + /** - * @magentoDataFixture Magento/Catalog/controllers/_files/products.php - * @magentoDbIsolation disabled + * Integration test for product view front action. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ViewTest extends \Magento\TestFramework\TestCase\AbstractController +class ViewTest extends AbstractController { /** + * @var ProductRepositoryInterface $productRepository + */ + private $productRepository; + + /** + * @var AttributeSetRepositoryInterface $attributeSetRepository + */ + private $attributeSetRepository; + + /** + * @var ProductAttributeRepositoryInterface $attributeSetRepository + */ + private $attributeRepository; + + /** + * @var Type $productEntityType + */ + private $productEntityType; + + /** @var Registry */ + private $registry; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var GetAttributeSetByName */ + private $getAttributeSetByName; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + $this->attributeSetRepository = $this->_objectManager->create(AttributeSetRepositoryInterface::class); + $this->attributeRepository = $this->_objectManager->create(ProductAttributeRepositoryInterface::class); + $this->productEntityType = $this->_objectManager->create(Type::class) + ->loadByCode(Product::ENTITY); + $this->registry = $this->_objectManager->get(Registry::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->getAttributeSetByName = $this->_objectManager->get(GetAttributeSetByName::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/controllers/_files/products.php * @magentoConfigFixture current_store catalog/seo/product_canonical_tag 1 + * @return void */ - public function testViewActionWithCanonicalTag() + public function testViewActionWithCanonicalTag(): void { $this->markTestSkipped( 'MAGETWO-40724: Canonical url from tests sometimes does not equal canonical url from action' @@ -26,4 +99,261 @@ public function testViewActionWithCanonicalTag() $this->getResponse()->getBody() ); } + + /** + * View product with custom attribute when attribute removed from it. + * + * It tests that after changing product attribute set from Default to Custom + * there are no warning messages in log in case Custom not contains attribute from Default. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_country_of_manufacture.php + * @magentoDataFixture Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture.php + * @return void + */ + public function testViewActionCustomAttributeSetWithoutCountryOfManufacture(): void + { + /** @var MockObject|LoggerInterface $logger */ + $logger = $this->setupLoggerMock(); + $product = $this->productRepository->get('simple_with_com'); + $attributeSetCustom = $this->getAttributeSetByName->execute('custom_attribute_set_wout_com'); + $product->setAttributeSetId($attributeSetCustom->getAttributeSetId()); + $this->productRepository->save($product); + + /** @var ProductAttributeInterface $attributeCountryOfManufacture */ + $attributeCountryOfManufacture = $this->attributeRepository->get('country_of_manufacture'); + $logger->expects($this->never()) + ->method('warning') + ->with( + "Attempt to load value of nonexistent EAV attribute", + [ + 'attribute_id' => $attributeCountryOfManufacture->getAttributeId(), + 'entity_type' => ProductInterface::class, + ] + ); + + $this->dispatch(sprintf('catalog/product/view/id/%s/', $product->getId())); + } + + /** + * @magentoDataFixture Magento/Quote/_files/is_not_salable_product.php + * @return void + */ + public function testDisabledProductInvisibility(): void + { + $product = $this->productRepository->get('simple-99'); + $this->dispatch(sprintf('catalog/product/view/id/%s/', $product->getId())); + + $this->assert404NotFound(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @dataProvider productVisibilityDataProvider + * @param int $visibility + * @return void + */ + public function testProductVisibility(int $visibility): void + { + $product = $this->updateProductVisibility('simple2', $visibility); + $this->dispatch(sprintf('catalog/product/view/id/%s/', $product->getId())); + + $this->assertProductIsVisible($product); + } + + /** + * @return array + */ + public function productVisibilityDataProvider(): array + { + return [ + 'catalog_search' => [Visibility::VISIBILITY_BOTH], + 'search' => [Visibility::VISIBILITY_IN_SEARCH], + 'catalog' => [Visibility::VISIBILITY_IN_CATALOG], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_products_not_visible_individually.php + */ + public function testProductNotVisibleIndividually(): void + { + $product = $this->updateProductVisibility('simple_not_visible_1', Visibility::VISIBILITY_NOT_VISIBLE); + $this->dispatch(sprintf('catalog/product/view/id/%s/', $product->getId())); + + $this->assert404NotFound(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_two_websites.php + * @magentoDbIsolation disabled + * @return void + */ + public function testProductVisibleOnTwoWebsites(): void + { + $currentStore = $this->storeManager->getStore(); + $product = $this->productRepository->get('simple-on-two-websites'); + $secondStoreId = $this->storeManager->getStore('fixture_second_store')->getId(); + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + $this->assertProductIsVisible($product); + $this->cleanUpCachedObjects(); + + try { + $this->storeManager->setCurrentStore($secondStoreId); + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + $this->assertProductIsVisible($product); + } finally { + $this->storeManager->setCurrentStore($currentStore); + } + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_two_websites.php + * @magentoDbIsolation disabled + * @return void + */ + public function testRemoveProductFromOneWebsiteVisibility(): void + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $currentStore = $this->storeManager->getStore(); + $secondStoreId = $this->storeManager->getStore('fixture_second_store')->getId(); + $product = $this->updateProduct('simple-on-two-websites', ['website_ids' => [$websiteId]]); + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + $this->assert404NotFound(); + $this->cleanUpCachedObjects(); + + try { + $this->storeManager->setCurrentStore($secondStoreId); + + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + $this->assertProductIsVisible($product); + } finally { + $this->storeManager->setCurrentStore($currentStore->getId()); + } + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_two_websites.php + * @magentoDbIsolation disabled + * @return void + */ + public function testProductAttributeByStores(): void + { + $secondStoreId = $this->storeManager->getStore('fixture_second_store')->getId(); + $product = $this->productRepository->get('simple-on-two-websites'); + $currentStoreId = $this->storeManager->getStore()->getId(); + + try { + $this->storeManager->setCurrentStore($secondStoreId); + $product = $this->updateProduct($product, ['status' => 2]); + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + $this->assert404NotFound(); + $this->cleanUpCachedObjects(); + $this->storeManager->setCurrentStore($currentStoreId); + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + $this->assertProductIsVisible($product); + } finally { + $this->storeManager->setCurrentStore($currentStoreId); + } + } + + /** + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testProductWithoutWebsite(): void + { + $product = $this->updateProduct('simple2', ['website_ids' => []]); + $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getId())); + + $this->assert404NotFound(); + } + + /** + * @param string|ProductInterface $product + * @param array $data + * @return ProductInterface + */ + public function updateProduct($product, array $data): ProductInterface + { + $product = is_string($product) ? $this->productRepository->get($product) : $product; + $product->addData($data); + + return $this->productRepository->save($product); + } + + /** + * @inheritdoc + */ + public function assert404NotFound() + { + parent::assert404NotFound(); + + $this->assertNull($this->registry->registry('current_product')); + } + + /** + * Assert that product is available in storefront + * + * @param ProductInterface $product + * @return void + */ + private function assertProductIsVisible(ProductInterface $product): void + { + $this->assertEquals( + Response::STATUS_CODE_200, + $this->getResponse()->getHttpResponseCode(), + 'Wrong response code is returned' + ); + $currentProduct = $this->registry->registry('current_product'); + $this->assertNotNull($currentProduct); + $this->assertEquals( + $product->getSku(), + $currentProduct->getSku(), + 'Wrong product is registered' + ); + } + + /** + * Clean up cached objects. + * + * @return void + */ + private function cleanUpCachedObjects(): void + { + $this->_objectManager->removeSharedInstance(Http::class); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_objectManager->removeSharedInstance(Response::class); + $this->_request = null; + $this->_response = null; + } + + /** + * Setup logger mock to check there are no warning messages logged. + * + * @return MockObject + */ + private function setupLoggerMock(): MockObject + { + $logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_objectManager->addSharedInstance($logger, MagentoMonologLogger::class); + + return $logger; + } + + /** + * Update product visibility + * + * @param string $sku + * @param int $visibility + * @return ProductInterface + */ + private function updateProductVisibility(string $sku, int $visibility): ProductInterface + { + $product = $this->productRepository->get($sku); + $product->setVisibility($visibility); + + return $this->productRepository->save($product); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php index ca9db3f28a91b..20805271f6b5b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php @@ -3,92 +3,114 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + -/** - * Test class for \Magento\Catalog\Controller\Product. - */ namespace Magento\Catalog\Controller; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Session; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\TestCase\AbstractController; + /** - * @magentoAppIsolation enabled + * Checks product view on storefront + * + * @see \Magento\Catalog\Controller\Product + * + * @magentoDbIsolation enabled */ -class ProductTest extends \Magento\TestFramework\TestCase\AbstractController +class ProductTest extends AbstractController { + /** @var Registry */ + private $registry; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Session */ + private $session; + + /** + * @inheritdoc + */ protected function setUp() { if (defined('HHVM_VERSION')) { $this->markTestSkipped('Randomly fails due to known HHVM bug (DOMText mixed with DOMElement)'); } parent::setUp(); + + $this->registry = $this->_objectManager->get(Registry::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->session = $this->_objectManager->get(Session::class); } + /** + * @inheritdoc + */ public function assert404NotFound() { parent::assert404NotFound(); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->assertNull($objectManager->get(\Magento\Framework\Registry::class)->registry('current_product')); + + $this->assertNull($this->registry->registry('current_product')); } - protected function _getProductImageFile() + /** + * Get product image file + * + * @return string + */ + protected function getProductImageFile(): string { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** - * @var $repository \Magento\Catalog\Model\ProductRepository - */ - $repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); - $product = $repository->get('simple_product_1'); + $product = $this->productRepository->get('simple_product_1'); $images = $product->getMediaGalleryImages()->getItems(); $image = reset($images); + return $image['file']; } /** * @magentoDataFixture Magento/Catalog/controllers/_files/products.php * @magentoAppArea frontend + * @return void */ - public function testViewAction() + public function testViewAction(): void { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** - * @var $repository \Magento\Catalog\Model\ProductRepository - */ - $repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); - $product = $repository->get('simple_product_1'); + $product = $this->productRepository->get('simple_product_1'); $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getEntityId())); + $currentProduct = $this->registry->registry('current_product'); - /** @var $currentProduct \Magento\Catalog\Model\Product */ - $currentProduct = $objectManager->get(\Magento\Framework\Registry::class)->registry('current_product'); - $this->assertInstanceOf(\Magento\Catalog\Model\Product::class, $currentProduct); + $this->assertInstanceOf(ProductInterface::class, $currentProduct); $this->assertEquals($product->getEntityId(), $currentProduct->getEntityId()); - - $lastViewedProductId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Session::class - )->getLastViewedProductId(); - $this->assertEquals($product->getEntityId(), $lastViewedProductId); + $this->assertEquals($product->getEntityId(), $this->session->getLastViewedProductId()); $responseBody = $this->getResponse()->getBody(); /* Product info */ - $this->assertContains('Simple Product 1 Name', $responseBody); - $this->assertContains('Simple Product 1 Full Description', $responseBody); - $this->assertContains('Simple Product 1 Short Description', $responseBody); + $this->assertContains($product->getName(), $responseBody); + $this->assertContains($product->getDescription(), $responseBody); + $this->assertContains($product->getShortDescription(), $responseBody); + $this->assertContains($product->getSku(), $responseBody); /* Stock info */ $this->assertContains('$1,234.56', $responseBody); $this->assertContains('In stock', $responseBody); - $this->assertContains('Add to Cart', $responseBody); + $this->assertContains((string)__('Add to Cart'), $responseBody); /* Meta info */ $this->assertContains('<title>Simple Product 1 Meta Title', $responseBody); $this->assertEquals( 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + Xpath::getElementsCountForXpath( '//meta[@name="keywords" and @content="Simple Product 1 Meta Keyword"]', $responseBody ) ); $this->assertEquals( 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + Xpath::getElementsCountForXpath( '//meta[@name="description" and @content="Simple Product 1 Meta Description"]', $responseBody ) @@ -97,34 +119,36 @@ public function testViewAction() /** * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void */ - public function testViewActionConfigurable() + public function testViewActionConfigurable(): void { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** - * @var $repository \Magento\Catalog\Model\ProductRepository - */ - $repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); - $product = $repository->get('simple'); + $product = $this->productRepository->get('simple'); $this->dispatch(sprintf('catalog/product/view/id/%s', $product->getEntityId())); $html = $this->getResponse()->getBody(); $this->assertEquals( 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + Xpath::getElementsCountForXpath( '//*[@id="product-options-wrapper"]', $html ) ); } - public function testViewActionNoProductId() + /** + * @return void + */ + public function testViewActionNoProductId(): void { $this->dispatch('catalog/product/view/id/'); + $this->assert404NotFound(); } - public function testViewActionRedirect() + /** + * @return void + */ + public function testViewActionRedirect(): void { $this->dispatch('catalog/product/view/?store=default'); @@ -133,30 +157,31 @@ public function testViewActionRedirect() /** * @magentoDataFixture Magento/Catalog/controllers/_files/products.php + * @return void */ - public function testGalleryAction() + public function testGalleryAction(): void { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** - * @var $repository \Magento\Catalog\Model\ProductRepository - */ - $repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); - $product = $repository->get('simple_product_1'); + $product = $this->productRepository->get('simple_product_1'); $this->dispatch(sprintf('catalog/product/gallery/id/%s', $product->getEntityId())); $this->assertContains('http://localhost/pub/media/catalog/product/', $this->getResponse()->getBody()); - $this->assertContains($this->_getProductImageFile(), $this->getResponse()->getBody()); + $this->assertContains($this->getProductImageFile(), $this->getResponse()->getBody()); } - public function testGalleryActionRedirect() + /** + * @return void + */ + public function testGalleryActionRedirect(): void { $this->dispatch('catalog/product/gallery/?store=default'); $this->assertRedirect(); } - public function testGalleryActionNoProduct() + /** + * @return void + */ + public function testGalleryActionNoProduct(): void { $this->dispatch('catalog/product/gallery/id/'); @@ -165,13 +190,14 @@ public function testGalleryActionNoProduct() /** * @magentoDataFixture Magento/Catalog/controllers/_files/products.php + * @return void */ - public function testImageAction() + public function testImageAction(): void { $this->markTestSkipped("All logic has been cut to avoid possible malicious usage of the method"); ob_start(); /* Preceding slash in URL is required in this case */ - $this->dispatch('/catalog/product/image' . $this->_getProductImageFile()); + $this->dispatch('/catalog/product/image' . $this->getProductImageFile()); $imageContent = ob_get_clean(); /** * Check against PNG file signature. @@ -180,10 +206,44 @@ public function testImageAction() $this->assertStringStartsWith(sprintf("%cPNG\r\n%c\n", 137, 26), $imageContent); } - public function testImageActionNoImage() + /** + * @return void + */ + public function testImageActionNoImage(): void { $this->dispatch('catalog/product/image/'); $this->assert404NotFound(); } + + /** + * Check that custom layout update files is employed. + * + * @magentoDataFixture Magento/Catalog/controllers/_files/products.php + * @return void + */ + public function testViewWithCustomUpdate(): void + { + //Setting a fake file for the product. + $file = 'test-file'; + /** @var ProductRepositoryInterface $repository */ + $repository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + $sku = 'simple_product_1'; + $product = $repository->get($sku); + $productId = $product->getId(); + /** @var ProductLayoutUpdateManager $layoutManager */ + $layoutManager = Bootstrap::getObjectManager()->get(ProductLayoutUpdateManager::class); + $layoutManager->setFakeFiles((int)$productId, [$file]); + //Updating the custom attribute. + $product->setCustomAttribute('custom_layout_update_file', $file); + $repository->save($product); + + //Viewing the product + $this->dispatch("catalog/product/view/id/$productId"); + //Layout handles must contain the file. + $handles = Bootstrap::getObjectManager()->get(\Magento\Framework\View\LayoutInterface::class) + ->getUpdate() + ->getHandles(); + $this->assertContains("catalog_product_view_selectable_{$sku}_{$file}", $handles); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php new file mode 100644 index 0000000000000..40725d3ee58be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php @@ -0,0 +1,121 @@ +category = $this->categoryFactory->create(); + $this->category->load(2); + } + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryFactory::class); + $this->recreateCategory(); + $this->attribute = $this->category->getAttributes()['custom_layout_update_file']->getBackend(); + $this->layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); + } + + /** + * Check that custom layout update file's values erase the old attribute's value. + * + * @return void + * @throws \Throwable + */ + public function testDependsOnNewUpdate(): void + { + //New selected file value is set + $this->layoutManager->setCategoryFakeFiles(2, ['new']); + $this->category->setCustomAttribute('custom_layout_update', 'test'); + $this->category->setOrigData('custom_layout_update', 'test'); + $this->category->setCustomAttribute('custom_layout_update_file', 'new'); + $this->attribute->beforeSave($this->category); + $this->assertEmpty($this->category->getCustomAttribute('custom_layout_update')->getValue()); + $this->assertEquals('new', $this->category->getCustomAttribute('custom_layout_update_file')->getValue()); + $this->assertEmpty($this->category->getData('custom_layout_update')); + $this->assertEquals('new', $this->category->getData('custom_layout_update_file')); + + //Existing update chosen + $this->recreateCategory(); + $this->category->setData('custom_layout_update', 'test'); + $this->category->setOrigData('custom_layout_update', 'test'); + $this->category->setData( + 'custom_layout_update_file', + \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::VALUE_USE_UPDATE_XML + ); + $this->attribute->beforeSave($this->category); + $this->assertEquals('test', $this->category->getData('custom_layout_update')); + /** @var AbstractBackend $fileAttribute */ + $fileAttribute = $this->category->getAttributes()['custom_layout_update_file']->getBackend(); + $fileAttribute->beforeSave($this->category); + $this->assertEquals(null, $this->category->getData('custom_layout_update_file')); + + //Removing custom layout update by explicitly selecting the new file (or an empty file). + $this->recreateCategory(); + $this->category->setData('custom_layout_update', 'test'); + $this->category->setOrigData('custom_layout_update', 'test'); + $this->category->setData( + 'custom_layout_update_file', + \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::VALUE_NO_UPDATE + ); + $this->attribute->beforeSave($this->category); + $this->assertEmpty($this->category->getData('custom_layout_update')); + + //Empty value doesn't change the old attribute. Any non-string value can be used to represent an empty value. + $this->recreateCategory(); + $this->category->setData('custom_layout_update', 'test'); + $this->category->setOrigData('custom_layout_update', 'test'); + $this->category->setData( + 'custom_layout_update_file', + false + ); + $this->attribute->beforeSave($this->category); + $this->assertEquals('test', $this->category->getData('custom_layout_update')); + $this->assertNull($this->category->getData('custom_layout_update_file')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/CustomlayoutupdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/CustomlayoutupdateTest.php new file mode 100644 index 0000000000000..7f594d265418f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/CustomlayoutupdateTest.php @@ -0,0 +1,124 @@ +category = $this->categoryFactory->create(); + $this->category->load(2); + } + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryFactory::class); + $this->recreateCategory(); + $this->attribute = $this->category->getAttributes()['custom_layout_update']->getBackend(); + } + + /** + * Test that attribute cannot be modified but only removed completely. + * + * @return void + * @throws \Throwable + * @magentoDbIsolation enabled + */ + public function testImmutable(): void + { + //Value is empty + $this->category->setCustomAttribute('custom_layout_update', false); + $this->category->setOrigData('custom_layout_update', null); + $this->attribute->beforeSave($this->category); + + //New value + $this->category->setCustomAttribute('custom_layout_update', 'test'); + $this->category->setOrigData('custom_layout_update', null); + $caughtException = false; + try { + $this->attribute->beforeSave($this->category); + } catch (LocalizedException $exception) { + $caughtException = true; + } + $this->assertTrue($caughtException); + $this->category->setCustomAttribute('custom_layout_update', 'testNew'); + $this->category->setOrigData('custom_layout_update', 'test'); + $caughtException = false; + try { + $this->attribute->beforeSave($this->category); + } catch (LocalizedException $exception) { + $caughtException = true; + } + $this->assertTrue($caughtException); + + //Removing a value + $this->category->setCustomAttribute('custom_layout_update', ''); + $this->category->setOrigData('custom_layout_update', 'test'); + $this->attribute->beforeSave($this->category); + $this->assertNull($this->category->getCustomAttribute('custom_layout_update')->getValue()); + + //Using old stored value + //Saving old value 1st + $this->recreateCategory(); + $this->category->setOrigData('custom_layout_update', 'test'); + $this->category->setData('custom_layout_update', 'test'); + $this->category->save(); + $this->recreateCategory(); + $this->category = $this->categoryFactory->create(['data' => $this->category->getData()]); + + //Trying the same value. + $this->category->setData('custom_layout_update', 'test'); + $this->attribute->beforeSave($this->category); + //Trying new value + $this->category->setData('custom_layout_update', 'test2'); + $caughtException = false; + try { + $this->attribute->beforeSave($this->category); + } catch (LocalizedException $exception) { + $caughtException = true; + } + $this->assertTrue($caughtException); + //Empty value + $this->category->setData('custom_layout_update', null); + $this->attribute->beforeSave($this->category); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php index b8e1f07364c28..6d66055cd1548 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php @@ -5,11 +5,20 @@ */ namespace Magento\Catalog\Model\Category; -use Magento\Catalog\Model\Category\DataProvider; -use Magento\Eav\Model\Config as EavConfig; +use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Registry; +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; -class DataProviderTest extends \PHPUnit\Framework\TestCase +/** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoAppArea adminhtml + */ +class DataProviderTest extends TestCase { /** * @var DataProvider @@ -17,18 +26,28 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase private $dataProvider; /** - * @var \Magento\Eav\Model\Entity\Type + * @var Registry */ - private $entityType; + private $registry; /** - * {@inheritDoc} + * @var CategoryFactory */ - protected function setUp() + private $categoryFactory; + + /** + * @var CategoryLayoutUpdateManager + */ + private $fakeFiles; + + /** + * Create subject instance. + * + * @return DataProvider + */ + private function createDataProvider(): DataProvider { - parent::setUp(); - $objectManager = Bootstrap::getObjectManager(); - $this->dataProvider = $objectManager->create( + return Bootstrap::getObjectManager()->create( DataProvider::class, [ 'name' => 'category_form_data_source', @@ -36,8 +55,19 @@ protected function setUp() 'requestFieldName' => 'id' ] ); + } - $this->entityType = $objectManager->create(EavConfig::class)->getEntityType('catalog_category'); + /** + * {@inheritDoc} + */ + protected function setUp() + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->dataProvider = $this->createDataProvider(); + $this->registry = $objectManager->get(Registry::class); + $this->categoryFactory = $objectManager->get(CategoryFactory::class); + $this->fakeFiles = $objectManager->get(CategoryLayoutUpdateManager::class); } /** @@ -59,4 +89,136 @@ public function testGetMetaRequiredAttributes() } } } + + /** + * Check that deprecated custom layout attribute is hidden. + * + * @return void + */ + public function testOldCustomLayoutInvisible(): void + { + //Testing a category without layout xml + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + $this->registry->register('category', $category); + + $meta = $this->dataProvider->getMeta(); + $this->assertArrayHasKey('design', $meta); + $this->assertArrayHasKey('children', $meta['design']); + $this->assertArrayHasKey('custom_layout_update', $meta['design']['children']); + $this->assertArrayHasKey('arguments', $meta['design']['children']['custom_layout_update']); + $this->assertArrayHasKey('data', $meta['design']['children']['custom_layout_update']['arguments']); + $this->assertArrayHasKey( + 'config', + $meta['design']['children']['custom_layout_update']['arguments']['data'] + ); + $config = $meta['design']['children']['custom_layout_update']['arguments']['data']['config']; + $this->assertTrue($config['visible'] === false); + } + + /** + * Check that custom layout update file attribute is processed correctly. + * + * @return void + */ + public function testCustomLayoutFileAttribute(): void + { + //File has value + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $id = 2; + $category->load($id); + $category->setData('custom_layout_update', null); + $category->setData('custom_layout_update_file', $file = 'test-file'); + $this->registry->register('category', $category); + $data = $this->dataProvider->getData(); + $this->assertEquals($file, $data[$id]['custom_layout_update_file']); + + //File has no value, the deprecated attribute does. + $this->dataProvider = $this->createDataProvider(); + $category->setData('custom_layout_update', $deprecated = 'test-deprecated'); + $category->setData('custom_layout_update_file', null); + $data = $this->dataProvider->getData(); + $this->assertEquals($deprecated, $data[$id]['custom_layout_update']); + $this->assertEquals(LayoutUpdate::VALUE_USE_UPDATE_XML, $data[$id]['custom_layout_update_file']); + } + + /** + * Extract custom layout update file attribute's options from metadata. + * + * @param array $meta + * @return array + */ + private function extractCustomLayoutOptions(array $meta): array + { + $this->assertArrayHasKey('design', $meta); + $this->assertArrayHasKey('children', $meta['design']); + $this->assertArrayHasKey('custom_layout_update_file', $meta['design']['children']); + $this->assertArrayHasKey('arguments', $meta['design']['children']['custom_layout_update_file']); + $this->assertArrayHasKey('data', $meta['design']['children']['custom_layout_update_file']['arguments']); + $this->assertArrayHasKey( + 'config', + $meta['design']['children']['custom_layout_update_file']['arguments']['data'] + ); + $this->assertArrayHasKey( + 'options', + $meta['design']['children']['custom_layout_update_file']['arguments']['data']['config'] + ); + + return $meta['design']['children']['custom_layout_update_file']['arguments']['data']['config']['options']; + } + + /** + * Check that proper options are returned for a category. + * + * @return void + */ + public function testCustomLayoutMeta(): void + { + //Testing a category without layout xml + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + $this->fakeFiles->setCategoryFakeFiles((int)$category->getId(), ['test1', 'test2']); + $this->registry->register('category', $category); + + $meta = $this->dataProvider->getMeta(); + $list = $this->extractCustomLayoutOptions($meta); + $expectedList = [ + [ + 'label' => 'No update', + 'value' => \Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate::VALUE_NO_UPDATE, + '__disableTmpl' => true + ], + ['label' => 'test1', 'value' => 'test1', '__disableTmpl' => true], + ['label' => 'test2', 'value' => 'test2', '__disableTmpl' => true] + ]; + sort($expectedList); + sort($list); + $this->assertEquals($expectedList, $list); + + //Product with old layout xml + $category->setCustomAttribute('custom_layout_update', 'test'); + $this->fakeFiles->setCategoryFakeFiles((int)$category->getId(), ['test3']); + + $meta = $this->dataProvider->getMeta(); + $expectedList = [ + [ + 'label' => 'No update', + 'value' => \Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate::VALUE_NO_UPDATE, + '__disableTmpl' => true + ], + [ + 'label' => 'Use existing', + 'value' => LayoutUpdate::VALUE_USE_UPDATE_XML, + '__disableTmpl' => true + ], + ['label' => 'test3', 'value' => 'test3', '__disableTmpl' => true], + ]; + $list = $this->extractCustomLayoutOptions($meta); + sort($expectedList); + sort($list); + $this->assertEquals($expectedList, $list); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php index f1e235f8c9bf2..e1e4a87c033d0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -7,14 +7,14 @@ namespace Magento\Catalog\Model; -use Magento\Backend\Model\Auth; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Catalog\Api\Data\CategoryInterfaceFactory; -use Magento\Framework\Acl\Builder; +use Magento\Catalog\Api\CategoryRepositoryInterfaceFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Magento\TestFramework\Bootstrap as TestBootstrap; /** * Provide tests for CategoryRepository model. @@ -22,26 +22,24 @@ class CategoryRepositoryTest extends TestCase { /** - * Test subject. - * - * @var CategoryRepositoryInterface + * @var CategoryLayoutUpdateManager */ - private $repo; + private $layoutManager; /** - * @var Auth + * @var CategoryRepositoryInterfaceFactory */ - private $auth; + private $repositoryFactory; /** - * @var Builder + * @var CollectionFactory */ - private $aclBuilder; + private $productCollectionFactory; /** - * @var CategoryInterfaceFactory + * @var CategoryCollectionFactory */ - private $categoryFactory; + private $categoryCollectionFactory; /** * Sets up common objects. @@ -50,63 +48,82 @@ class CategoryRepositoryTest extends TestCase */ protected function setUp() { - $this->repo = Bootstrap::getObjectManager()->create(CategoryRepositoryInterface::class); - $this->auth = Bootstrap::getObjectManager()->get(Auth::class); - $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); - $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryInterfaceFactory::class); + $this->repositoryFactory = Bootstrap::getObjectManager()->get(CategoryRepositoryInterfaceFactory::class); + $this->layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + $this->categoryCollectionFactory = Bootstrap::getObjectManager()->create(CategoryCollectionFactory::class); } /** - * @inheritDoc + * Create subject object. + * + * @return CategoryRepositoryInterface */ - protected function tearDown() + private function createRepo(): CategoryRepositoryInterface { - parent::tearDown(); - - $this->auth->logout(); - $this->aclBuilder->resetRuntimeAcl(); + return $this->repositoryFactory->create(); } /** - * Test authorization when saving category's design settings. + * Test that custom layout file attribute is saved. * + * @return void + * @throws \Throwable * @magentoDataFixture Magento/Catalog/_files/category.php - * @magentoAppArea adminhtml * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ - public function testSaveDesign() + public function testCustomLayout(): void { - $category = $this->repo->get(333); - $this->auth->login(TestBootstrap::ADMIN_NAME, TestBootstrap::ADMIN_PASSWORD); - - //Admin doesn't have access to category's design. - $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_category_design'); - - $category->setCustomAttribute('custom_design', 2); - $category = $this->repo->save($category); - $customDesignAttribute = $category->getCustomAttribute('custom_design'); - $this->assertTrue(!$customDesignAttribute || !$customDesignAttribute->getValue()); - - //Admin has access to category' design. - $this->aclBuilder->getAcl() - ->allow(null, ['Magento_Catalog::categories', 'Magento_Catalog::edit_category_design']); + //New valid value + $repo = $this->createRepo(); + $category = $repo->get(333); + $newFile = 'test'; + $this->layoutManager->setCategoryFakeFiles(333, [$newFile]); + $category->setCustomAttribute('custom_layout_update_file', $newFile); + $repo->save($category); + $repo = $this->createRepo(); + $category = $repo->get(333); + $this->assertEquals($newFile, $category->getCustomAttribute('custom_layout_update_file')->getValue()); - $category->setCustomAttribute('custom_design', 2); - $category = $this->repo->save($category); - $this->assertNotEmpty($category->getCustomAttribute('custom_design')); - $this->assertEquals(2, $category->getCustomAttribute('custom_design')->getValue()); + //Setting non-existent value + $newFile = 'does not exist'; + $category->setCustomAttribute('custom_layout_update_file', $newFile); + $caughtException = false; + try { + $repo->save($category); + } catch (LocalizedException $exception) { + $caughtException = true; + } + $this->assertTrue($caughtException); + } - //Creating a new one - /** @var CategoryInterface $newCategory */ - $newCategory = $this->categoryFactory->create(); - $newCategory->setName('new category without design'); - $newCategory->setParentId($category->getParentId()); - $newCategory->setIsActive(true); - $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_category_design'); - $newCategory->setCustomAttribute('custom_design', 2); - $newCategory = $this->repo->save($newCategory); - $customDesignAttribute = $newCategory->getCustomAttribute('custom_design'); - $this->assertTrue(!$customDesignAttribute || !$customDesignAttribute->getValue()); + /** + * Test removal of categories. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/categories.php + * @magentoAppArea adminhtml + * @return void + */ + public function testCategoryBehaviourAfterDelete(): void + { + $productCollection = $this->productCollectionFactory->create(); + $deletedCategories = ['3', '4', '5', '13']; + $categoryCollectionIds = $this->categoryCollectionFactory->create()->getAllIds(); + $this->createRepo()->deleteByIdentifier(3); + $this->assertEquals( + 0, + $productCollection->addCategoriesFilter(['in' => $deletedCategories])->getSize(), + 'The category-products relations was not deleted after category delete' + ); + $newCategoryCollectionIds = $this->categoryCollectionFactory->create()->getAllIds(); + $difference = array_diff($categoryCollectionIds, $newCategoryCollectionIds); + sort($difference); + $this->assertEquals( + $deletedCategories, + $difference, + 'Wrong categories was deleted' + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 1d7936d740b8d..5ec0427093997 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php @@ -3,26 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category as Category; +use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\Tree; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; +use Magento\Framework\Url; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * Test class for \Magento\Catalog\Model\Category. * - general behaviour is tested * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @see \Magento\Catalog\Model\CategoryTreeTest * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ -class CategoryTest extends \PHPUnit\Framework\TestCase +class CategoryTest extends TestCase { /** - * @var \Magento\Store\Model\Store + * @var Store */ protected $_store; /** - * @var \Magento\Catalog\Model\Category + * @var Category */ protected $_model; @@ -31,50 +49,61 @@ class CategoryTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** @var CategoryRepository */ + private $categoryResource; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** + * @inheritdoc + */ protected function setUp() { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var $storeManager \Magento\Store\Model\StoreManagerInterface */ - $storeManager = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + /** @var $storeManager StoreManagerInterface */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->_store = $storeManager->getStore(); - $this->_model = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + $this->_model = $this->objectManager->create(Category::class); + $this->categoryResource = $this->objectManager->get(CategoryResource::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); } - public function testGetUrlInstance() + public function testGetUrlInstance(): void { $instance = $this->_model->getUrlInstance(); - $this->assertInstanceOf(\Magento\Framework\Url::class, $instance); + $this->assertInstanceOf(Url::class, $instance); $this->assertSame($instance, $this->_model->getUrlInstance()); } - public function testGetTreeModel() + public function testGetTreeModel(): void { $model = $this->_model->getTreeModel(); - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Category\Tree::class, $model); + $this->assertInstanceOf(Tree::class, $model); $this->assertNotSame($model, $this->_model->getTreeModel()); } - public function testGetTreeModelInstance() + public function testGetTreeModelInstance(): void { $model = $this->_model->getTreeModelInstance(); - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Category\Tree::class, $model); + $this->assertInstanceOf(Tree::class, $model); $this->assertSame($model, $this->_model->getTreeModelInstance()); } - public function testGetDefaultAttributeSetId() + public function testGetDefaultAttributeSetId(): void { /* based on value installed in DB */ $this->assertEquals(3, $this->_model->getDefaultAttributeSetId()); } - public function testGetProductCollection() + public function testGetProductCollection(): void { $collection = $this->_model->getProductCollection(); - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Product\Collection::class, $collection); + $this->assertInstanceOf(ProductCollection::class, $collection); $this->assertEquals($this->_model->getStoreId(), $collection->getStoreId()); } - public function testGetAttributes() + public function testGetAttributes(): void { $attributes = $this->_model->getAttributes(); $this->assertArrayHasKey('name', $attributes); @@ -85,7 +114,7 @@ public function testGetAttributes() $this->assertArrayNotHasKey('custom_design', $attributes); } - public function testGetProductsPosition() + public function testGetProductsPosition(): void { $this->assertEquals([], $this->_model->getProductsPosition()); $this->_model->unsetData(); @@ -97,23 +126,21 @@ public function testGetProductsPosition() $this->assertNotEmpty($this->_model->getProductsPosition()); } - public function testGetStoreIds() + public function testGetStoreIds(): void { $this->_model = $this->getCategoryByName('Category 1.1'); /* id from fixture */ $this->assertContains( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore()->getId(), + Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId(), $this->_model->getStoreIds() ); } - public function testSetGetStoreId() + public function testSetGetStoreId(): void { $this->assertEquals( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class + Bootstrap::getObjectManager()->get( + StoreManagerInterface::class )->getStore()->getId(), $this->_model->getStoreId() ); @@ -126,10 +153,10 @@ public function testSetGetStoreId() * @magentoAppIsolation enabled * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 */ - public function testSetStoreIdWithNonNumericValue() + public function testSetStoreIdWithNonNumericValue(): void { - /** @var $store \Magento\Store\Model\Store */ - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + /** @var $store Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); $store->load('fixturestore'); $this->assertNotEquals($this->_model->getStoreId(), $store->getId()); @@ -139,7 +166,7 @@ public function testSetStoreIdWithNonNumericValue() $this->assertEquals($this->_model->getStoreId(), $store->getId()); } - public function testGetUrl() + public function testGetUrl(): void { $this->assertStringEndsWith('catalog/category/view/', $this->_model->getUrl()); @@ -156,42 +183,42 @@ public function testGetUrl() $this->assertStringEndsWith('catalog/category/view/id/1000/', $this->_model->getUrl()); } - public function testGetCategoryIdUrl() + public function testGetCategoryIdUrl(): void { $this->assertStringEndsWith('catalog/category/view/', $this->_model->getCategoryIdUrl()); $this->_model->setUrlKey('test_key'); $this->assertStringEndsWith('catalog/category/view/s/test_key/', $this->_model->getCategoryIdUrl()); } - public function testFormatUrlKey() + public function testFormatUrlKey(): void { $this->assertEquals('test', $this->_model->formatUrlKey('test')); $this->assertEquals('test-some-chars-5', $this->_model->formatUrlKey('test-some#-chars^5')); $this->assertEquals('test', $this->_model->formatUrlKey('test-????????')); } - public function testGetImageUrl() + public function testGetImageUrl(): void { $this->assertFalse($this->_model->getImageUrl()); $this->_model->setImage('test.gif'); $this->assertStringEndsWith('media/catalog/category/test.gif', $this->_model->getImageUrl()); } - public function testGetCustomDesignDate() + public function testGetCustomDesignDate(): void { $dates = $this->_model->getCustomDesignDate(); $this->assertArrayHasKey('from', $dates); $this->assertArrayHasKey('to', $dates); } - public function testGetDesignAttributes() + public function testGetDesignAttributes(): void { $attributes = $this->_model->getDesignAttributes(); $this->assertContains('custom_design_from', array_keys($attributes)); $this->assertContains('custom_design_to', array_keys($attributes)); } - public function testCheckId() + public function testCheckId(): void { $this->_model = $this->getCategoryByName('Category 1.1.1'); $categoryId = $this->_model->getId(); @@ -199,13 +226,13 @@ public function testCheckId() $this->assertFalse($this->_model->checkId(111)); } - public function testVerifyIds() + public function testVerifyIds(): void { $ids = $this->_model->verifyIds($this->_model->getParentIds()); $this->assertNotContains(100, $ids); } - public function testHasChildren() + public function testHasChildren(): void { $this->_model->load(3); $this->assertTrue($this->_model->hasChildren()); @@ -213,21 +240,21 @@ public function testHasChildren() $this->assertFalse($this->_model->hasChildren()); } - public function testGetRequestPath() + public function testGetRequestPath(): void { $this->assertNull($this->_model->getRequestPath()); $this->_model->setData('request_path', 'test'); $this->assertEquals('test', $this->_model->getRequestPath()); } - public function testGetName() + public function testGetName(): void { $this->assertNull($this->_model->getName()); $this->_model->setData('name', 'test'); $this->assertEquals('test', $this->_model->getName()); } - public function testGetProductCount() + public function testGetProductCount(): void { $this->_model->load(6); $this->assertEquals(0, $this->_model->getProductCount()); @@ -236,14 +263,14 @@ public function testGetProductCount() $this->assertEquals(1, $this->_model->getProductCount()); } - public function testGetAvailableSortBy() + public function testGetAvailableSortBy(): void { $this->assertEquals([], $this->_model->getAvailableSortBy()); $this->_model->setData('available_sort_by', 'test,and,test'); $this->assertEquals(['test', 'and', 'test'], $this->_model->getAvailableSortBy()); } - public function testGetAvailableSortByOptions() + public function testGetAvailableSortByOptions(): void { $options = $this->_model->getAvailableSortByOptions(); $this->assertContains('price', array_keys($options)); @@ -251,25 +278,27 @@ public function testGetAvailableSortByOptions() $this->assertContains('name', array_keys($options)); } - public function testGetDefaultSortBy() + public function testGetDefaultSortBy(): void { $this->assertEquals('position', $this->_model->getDefaultSortBy()); } - public function testValidate() + public function testValidate(): void { - $this->_model->addData([ - "include_in_menu" => false, - "is_active" => false, - 'name' => 'test', - ]); + $this->_model->addData( + [ + "include_in_menu" => false, + "is_active" => false, + 'name' => 'test', + ] + ); $this->assertNotEmpty($this->_model->validate()); } /** * @magentoDataFixture Magento/Catalog/_files/category_with_position.php */ - public function testSaveCategoryWithPosition() + public function testSaveCategoryWithPosition(): void { $category = $this->_model->load('444'); $this->assertEquals('5', $category->getPosition()); @@ -278,10 +307,10 @@ public function testSaveCategoryWithPosition() /** * @magentoDbIsolation enabled */ - public function testSaveCategoryWithoutImage() + public function testSaveCategoryWithoutImage(): void { - $model = $this->objectManager->create(\Magento\Catalog\Model\Category::class); - $repository = $this->objectManager->get(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + $model = $this->objectManager->create(Category::class); + $repository = $this->objectManager->get(CategoryRepositoryInterface::class); $model->setName('Test Category 100') ->setParentId(2) @@ -299,7 +328,7 @@ public function testSaveCategoryWithoutImage() /** * @magentoAppArea adminhtml */ - public function testDeleteChildren() + public function testDeleteChildren(): void { $this->_model->unsetData(); $this->_model->load(4); @@ -320,29 +349,101 @@ public function testDeleteChildren() $this->assertEquals($this->_model->getId(), null); } + /** + * @magentoDataFixture Magento/Catalog/_files/category.php + */ + public function testAddChildCategory(): void + { + $data = [ + 'name' => 'Child Category', + 'path' => '1/2/333', + 'is_active' => '1', + 'include_in_menu' => '1', + ]; + $this->_model->setData($data); + $this->categoryResource->save($this->_model); + $parentCategory = $this->categoryRepository->get(333); + $this->assertContains($this->_model->getId(), $parentCategory->getChildren()); + } + + /** + * @return void + */ + public function testMissingRequiredAttribute(): void + { + $data = [ + 'path' => '1/2', + 'is_active' => '1', + 'include_in_menu' => '1', + ]; + $this->expectException(AttributeException::class); + $this->expectExceptionMessage( + (string)__('The "Name" attribute value is empty. Set the attribute and try again.') + ); + $this->_model->setData($data); + $this->_model->validate(); + } + + /** + * @dataProvider categoryFieldsProvider + * @param array $data + */ + public function testCategoryCreateWithDifferentFields(array $data): void + { + $requiredData = [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + ]; + $this->_model->setData(array_merge($requiredData, $data)); + $this->categoryResource->save($this->_model); + $category = $this->categoryRepository->get($this->_model->getId()); + $categoryData = $category->toArray(array_keys($data)); + $this->assertSame($data, $categoryData); + } + + /** + * @return array + */ + public function categoryFieldsProvider(): array + { + return [ + [ + 'enable_fields' => [ + 'is_active' => '1', + 'include_in_menu' => '1', + ], + 'disable_fields' => [ + 'is_active' => '0', + 'include_in_menu' => '0', + ], + ], + ]; + } + /** * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation disabled * @return void */ - public function testCreateSubcategoryWithMultipleStores() + public function testCreateSubcategoryWithMultipleStores(): void { $parentCategoryId = 3; - $storeManager = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); - $storeManager->setCurrentStore(\Magento\Store\Model\Store::ADMIN_CODE); - /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ - $storeRepository = $this->objectManager->get(\Magento\Store\Api\StoreRepositoryInterface::class); + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(Store::ADMIN_CODE); + /** @var StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); $storeId = $storeRepository->get('fixture_second_store')->getId(); - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface $repository */ - $repository = $this->objectManager->get(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + /** @var CategoryRepositoryInterface $repository */ + $repository = $this->objectManager->get(CategoryRepositoryInterface::class); $parentCategory = $repository->get($parentCategoryId, $storeId); $parentAllStoresPath = $parentCategory->getUrlPath(); $parentSecondStoreKey = 'parent-category-url-key-second-store'; $parentCategory->setUrlKey($parentSecondStoreKey); $repository->save($parentCategory); - /** @var \Magento\Catalog\Model\Category $childCategory */ - $childCategory = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + /** @var Category $childCategory */ + $childCategory = $this->objectManager->create(Category::class); $childCategory->setName('Test Category 100') ->setParentId($parentCategoryId) ->setLevel(2) @@ -360,10 +461,10 @@ public function testCreateSubcategoryWithMultipleStores() protected function getCategoryByName($categoryName) { - /* @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ - - $collection = $this->objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); + /* @var Collection $collection */ + $collection = $this->objectManager->create(Collection::class); $collection->addNameToResult()->load(); + return $collection->getItemByColumnValue('name', $categoryName); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php index cb2f436ae90a0..58a7a0fbb2ace 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php @@ -246,6 +246,7 @@ public function testDeleteCategory() * * @magentoConfigFixture current_store catalog/frontend/flat_catalog_category true * @magentoAppArea frontend + * @magentoDbIsolation disabled */ public function testFlatAfterDeleted() { @@ -348,19 +349,21 @@ private function instantiateCategoryModel() */ private function createSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $category = $this->getLoadedDefaultCategory(); - - $categoryOne = $this->instantiateCategoryModel(); - $categoryOne->setName('Category One')->setPath($category->getPath())->setIsActive(true); - $category->getResource()->save($categoryOne); - self::$categoryOne = $categoryOne->getId(); - - $categoryTwo = $this->instantiateCategoryModel(); - $categoryTwo->setName('Category Two')->setPath($categoryOne->getPath())->setIsActive(true); - $category->getResource()->save($categoryTwo); - self::$categoryTwo = $categoryTwo->getId(); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $category = $this->getLoadedDefaultCategory(); + + $categoryOne = $this->instantiateCategoryModel(); + $categoryOne->setName('Category One')->setPath($category->getPath())->setIsActive(true); + $category->getResource()->save($categoryOne); + self::$categoryOne = $categoryOne->getId(); + + $categoryTwo = $this->instantiateCategoryModel(); + $categoryTwo->setName('Category Two')->setPath($categoryOne->getPath())->setIsActive(true); + $category->getResource()->save($categoryTwo); + self::$categoryTwo = $categoryTwo->getId(); + } + ); } /** @@ -371,11 +374,13 @@ private function createSubCategoriesInDefaultCategory() */ private function moveSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $this->createSubCategoriesInDefaultCategory(); - $categoryTwo = $this->getLoadedCategory(self::$categoryTwo); - $categoryTwo->move(self::$defaultCategoryId, self::$categoryOne); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $this->createSubCategoriesInDefaultCategory(); + $categoryTwo = $this->getLoadedCategory(self::$categoryTwo); + $categoryTwo->move(self::$defaultCategoryId, self::$categoryOne); + } + ); } /** @@ -386,10 +391,12 @@ private function moveSubCategoriesInDefaultCategory() */ private function deleteSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $this->createSubCategoriesInDefaultCategory(); - $this->removeSubCategoriesInDefaultCategory(); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $this->createSubCategoriesInDefaultCategory(); + $this->removeSubCategoriesInDefaultCategory(); + } + ); } /** @@ -398,13 +405,15 @@ private function deleteSubCategoriesInDefaultCategory() */ private function removeSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $category = $this->instantiateCategoryModel(); - $category->load(self::$categoryTwo); - $category->delete(); - $category->load(self::$categoryOne); - $category->delete(); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $category = $this->instantiateCategoryModel(); + $category->load(self::$categoryTwo); + $category->delete(); + $category->load(self::$categoryOne); + $category->delete(); + } + ); } /** @@ -468,12 +477,4 @@ private function getActiveConfigInstance() \Magento\Framework\App\Config\MutableScopeConfigInterface::class ); } - - /** - * teardown - */ - public function tearDown() - { - parent::tearDown(); - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php index d0a4f2ead4d6b..d1e040a307587 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php @@ -32,7 +32,7 @@ public static function setUpBeforeClass() protected function setUp() { - $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\Indexer\Product\Eav\Processor::class ); } @@ -46,24 +46,24 @@ protected function setUp() public function testReindexAll() { /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attr **/ - $attr = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class) + $attr = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class) ->getAttribute('catalog_product', 'weight'); $attr->setIsFilterable(1)->save(); $this->assertTrue($attr->isIndexable()); - $priceIndexerProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $priceIndexerProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\Indexer\Product\Price\Processor::class ); $priceIndexerProcessor->reindexAll(); $this->_processor->reindexAll(); - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Block\Product\ListProduct::class ); @@ -82,12 +82,4 @@ public function testReindexAll() $this->assertEquals(1, $product->getWeight()); } } - - /** - * teardown - */ - public function tearDown() - { - parent::tearDown(); - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php index 677135092526c..095fa864ccb41 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php @@ -11,6 +11,8 @@ use Magento\Catalog\Model\Indexer\Product\Flat\Processor; use Magento\Catalog\Model\Indexer\Product\Flat\State; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; @@ -35,6 +37,22 @@ class FullTest extends \Magento\TestFramework\Indexer\TestCase */ private $objectManager; + /** + * @inheritdoc + */ + public static function setUpBeforeClass() + { + /* + * Due to insufficient search engine isolation for Elasticsearch, this class must explicitly perform + * a fulltext reindex prior to running its tests. + * + * This should be removed upon completing MC-19455. + */ + $indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + $fulltextIndexer = $indexRegistry->get(Fulltext::INDEXER_ID); + $fulltextIndexer->reindexAll(); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php index bbaf453938705..9ae9cc6b6629f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php @@ -5,8 +5,10 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Flat; +use Magento\Catalog\Model\Product\Attribute\Repository; + /** - * Class FullTest + * Integration tests for \Magento\Catalog\Model\Indexer\Product\Flat\Processor. */ class ProcessorTest extends \Magento\TestFramework\Indexer\TestCase { @@ -64,22 +66,23 @@ public function testSaveAttribute() } /** - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 */ public function testDeleteAttribute() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - - /** @var \Magento\Catalog\Model\ResourceModel\Product $productResource */ - $productResource = $product->getResource(); - $productResource->getAttribute('media_gallery')->delete(); + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ + $model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); + /** @var Repository $productAttributeRepository */ + $productAttributeRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(Repository::class); + $productAttrubute = $productAttributeRepository->get('flat_attribute'); + $productAttributeId = $productAttrubute->getAttributeId(); + $model->load($productAttributeId)->delete(); $this->assertTrue($this->_processor->getIndexer()->isInvalid()); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php index a6d1aa5be3e37..d2fb64813dd1e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php @@ -81,6 +81,7 @@ public function getRangeItemCountsDataProvider() /** * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation disabled + * @magentoConfigFixture default/catalog/search/engine mysql * @dataProvider getRangeItemCountsDataProvider */ public function testGetRangeItemCounts($inputRange, $expectedItemCounts) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/_files/attribute_with_option.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/_files/attribute_with_option.php index 833d1b114a0b5..b4431678b2016 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/_files/attribute_with_option.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/_files/attribute_with_option.php @@ -20,6 +20,7 @@ 'is_global' => 1, 'frontend_input' => 'select', 'is_filterable' => 1, + 'is_user_defined' => 1, 'option' => ['value' => ['option_0' => [0 => 'Option Label']]], 'backend_type' => 'int', ] diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AbstractAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AbstractAttributeTest.php new file mode 100644 index 0000000000000..d2ab4d69dc45c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AbstractAttributeTest.php @@ -0,0 +1,196 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->attributeRepository = $this->objectManager->create(AttributeRepositoryInterface::class); + } + + /** + * @dataProvider productProvider + * @param $productSku + * @return void + */ + public function testSaveAttribute(string $productSku): void + { + $product = $this->setAttributeValueAndValidate($productSku, $this->getDefaultAttributeValue()); + $product = $this->productRepository->save($product); + $this->assertEquals($this->getDefaultAttributeValue(), $product->getData($this->getAttributeCode())); + } + + /** + * @dataProvider productProvider + * @param string $productSku + * @return void + */ + public function testRequiredAttribute(string $productSku): void + { + $this->expectException(Exception::class); + $messageFormat = 'The "%s" attribute value is empty. Set the attribute and try again.'; + $this->expectExceptionMessage( + (string)__(sprintf($messageFormat, $this->getAttribute()->getDefaultFrontendLabel())) + ); + $this->prepareAttribute(['is_required' => true]); + $this->unsetAttributeValueAndValidate($productSku); + } + + /** + * @dataProvider productProvider + * @param string $productSku + * @return void + */ + public function testDefaultValue(string $productSku): void + { + $this->prepareAttribute(['default_value' => $this->getDefaultAttributeValue()]); + $product = $this->unsetAttributeValueAndValidate($productSku); + $product = $this->productRepository->save($product); + $this->assertEquals($this->getDefaultAttributeValue(), $product->getData($this->getAttributeCode())); + } + + /** + * @dataProvider uniqueAttributeValueProvider + * @param string $firstSku + * @param string $secondSku + * @return void + */ + public function testUniqueAttribute(string $firstSku, string $secondSku): void + { + $this->expectException(Exception::class); + $messageFormat = 'The value of the "%s" attribute isn\'t unique. Set a unique value and try again.'; + $this->expectExceptionMessage( + (string)__(sprintf($messageFormat, $this->getAttribute()->getDefaultFrontendLabel())) + ); + $this->prepareAttribute(['is_unique' => 1]); + $product = $this->setAttributeValueAndValidate($firstSku, $this->getDefaultAttributeValue()); + $this->productRepository->save($product); + $this->setAttributeValueAndValidate($secondSku, $this->getDefaultAttributeValue()); + } + + /** + * Get attribute + * + * @return ProductAttributeInterface + */ + protected function getAttribute(): ProductAttributeInterface + { + if ($this->attribute === null) { + $this->attribute = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $this->getAttributeCode() + ); + } + + return $this->attribute; + } + + /** + * Set attribute value to product and validate the product + * + * @param string $attributeValue + * @param string $productSku + * @return ProductInterface + */ + protected function setAttributeValueAndValidate(string $productSku, string $attributeValue): ProductInterface + { + $product = $this->productRepository->get($productSku); + $product->addData([$this->getAttributeCode() => $attributeValue]); + $product->validate(); + + return $product; + } + + /** + * Unset attribute value of the product and validate the product + * + * @param string $productSku + * @return ProductInterface + */ + private function unsetAttributeValueAndValidate(string $productSku): ProductInterface + { + $product = $this->productRepository->get($productSku); + $product->unsetData($this->getAttributeCode()); + $product->validate(); + + return $product; + } + + /** + * Prepare attribute to test + * + * @param array $data + * @return void + */ + private function prepareAttribute(array $data): void + { + $attribute = $this->getAttribute(); + $attribute->addData($data); + $this->attributeRepository->save($attribute); + } + + /** + * Returns attribute code for current test + * + * @return string + */ + abstract protected function getAttributeCode(): string; + + /** + * Get default value for current attribute + * + * @return string + */ + abstract protected function getDefaultAttributeValue(): string; + + /** + * Products provider for tests + * + * @return array + */ + abstract public function productProvider(): array; + + /** + * Provider for unique attribute tests + * + * @return array + */ + abstract public function uniqueAttributeValueProvider(): array; +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeDateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeDateTest.php new file mode 100644 index 0000000000000..d30f32087c815 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeDateTest.php @@ -0,0 +1,79 @@ +markTestSkipped('Test is blocked by issue MC-28950'); + } + + /** + * @inheritdoc + */ + protected function getAttributeCode(): string + { + return 'date_attribute'; + } + + /** + * @inheritdoc + */ + protected function getDefaultAttributeValue(): string + { + return $this->getAttribute()->getBackend()->formatDate('11/20/19'); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_date_attribute.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * @dataProvider uniqueAttributeValueProvider + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + * @inheritdoc + */ + public function testUniqueAttribute(string $firstSku, string $secondSku): void + { + parent::testUniqueAttribute($firstSku, $secondSku); + } + + /** + * @inheritdoc + */ + public function productProvider(): array + { + return [ + [ + 'product_sku' => 'simple2', + ], + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeDropdownTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeDropdownTest.php new file mode 100644 index 0000000000000..c1cdd0bab28aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeDropdownTest.php @@ -0,0 +1,70 @@ +getAttribute()->getSource()->getOptionId('Option 1'); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * @dataProvider uniqueAttributeValueProvider + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + * @inheritdoc + */ + public function testUniqueAttribute(string $firstSku, string $secondSku): void + { + parent::testUniqueAttribute($firstSku, $secondSku); + } + + /** + * @inheritdoc + */ + public function productProvider(): array + { + return [ + [ + 'product_sku' => 'simple2', + ], + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeMultiSelectTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeMultiSelectTest.php new file mode 100644 index 0000000000000..4ee2b83010bdc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeMultiSelectTest.php @@ -0,0 +1,79 @@ +getAttribute()->getSource()->getOptionId('Option 1'); + } + + /** + * @inheritdoc + * @dataProvider productProvider + */ + public function testDefaultValue(string $productSku): void + { + $this->markTestSkipped('Test is blocked by issue MC-29019'); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/multiselect_attribute.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * @dataProvider uniqueAttributeValueProvider + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + * @inheritdoc + */ + public function testUniqueAttribute(string $firstSku, string $secondSku): void + { + parent::testUniqueAttribute($firstSku, $secondSku); + } + + /** + * @inheritdoc + */ + public function productProvider(): array + { + return [ + [ + 'product_sku' => 'simple2', + ], + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributePriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributePriceTest.php new file mode 100644 index 0000000000000..5de9d30f71638 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributePriceTest.php @@ -0,0 +1,93 @@ +markTestSkipped('Test is blocked by issue MC-29018'); + parent::testUniqueAttribute($firstSku, $secondSku); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_decimal_attribute.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testNegativeValue(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage((string)__('Please enter a number 0 or greater in this field.')); + $this->setAttributeValueAndValidate('simple2', '-1'); + } + + /** + * @dataProvider productProvider + * @param string $productSku + */ + public function testDefaultValue(string $productSku): void + { + // product price attribute does not support default value + } + + /** + * @inheritdoc + */ + public function productProvider(): array + { + return [ + [ + 'product_sku' => 'simple2', + ], + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } + + /** + * @inheritdoc + */ + protected function getAttributeCode(): string + { + return 'decimal_attribute'; + } + + /** + * @inheritdoc + */ + protected function getDefaultAttributeValue(): string + { + return '100'; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeTextAreaTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeTextAreaTest.php new file mode 100644 index 0000000000000..61dbf78962c9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeTextAreaTest.php @@ -0,0 +1,70 @@ + 'simple2', + ] + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeTextTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeTextTest.php new file mode 100644 index 0000000000000..4da09bc1eca5a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeTextTest.php @@ -0,0 +1,70 @@ + 'simple2', + ], + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeYesNoTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeYesNoTest.php new file mode 100644 index 0000000000000..7b966791c7b6e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Save/AttributeYesNoTest.php @@ -0,0 +1,70 @@ + 'simple2', + ], + ]; + } + + /** + * @inheritdoc + */ + public function uniqueAttributeValueProvider(): array + { + return [ + [ + 'first_product_sku' => 'simple2', + 'second_product_sku' => 'simple-out-of-stock', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/SetTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/SetTest.php new file mode 100644 index 0000000000000..d5a8b694b7718 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/SetTest.php @@ -0,0 +1,276 @@ +objectManager = Bootstrap::getObjectManager(); + $this->setRepository = $this->objectManager->get(AttributeSetRepositoryInterface::class); + $this->attributeRepository = $this->objectManager->create(ProductAttributeRepositoryInterface::class); + $this->config = $this->objectManager->get(Config::class); + $this->defaultSetId = (int)$this->config->getEntityType(Product::ENTITY)->getDefaultAttributeSetId(); + $this->attributeSetResource = $this->objectManager->get(AttributeSetResource::class); + $this->attributeCollectionFactory = $this->objectManager->get(CollectionFactory ::class); + $this->attributeGroupByName = $this->objectManager->get(GetAttributeGroupByName::class); + $this->getEntityIdByAttributeId = $this->objectManager->get(GetEntityIdByAttributeId::class); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + * @dataProvider addAttributeToSetDataProvider + * @param string $groupName + * @param string $attributeCode + * @return void + */ + public function testSaveWithGroupsAndAttributes(string $groupName, string $attributeCode): void + { + $set = $this->setRepository->get($this->defaultSetId); + $attributeGroup = $this->getAttributeGroup($groupName); + $groupId = $attributeGroup ? $attributeGroup->getAttributeGroupId() : 'ynode-1'; + $attributeId = (int)$this->attributeRepository->get($attributeCode)->getAttributeId(); + $additional = [ + 'attributes' => [ + [$attributeId, $groupId, 1], + ], + 'groups' => [ + [$groupId, $groupName, 1], + ], + ]; + $set->organizeData($this->getAttributeSetData($additional)); + $this->attributeSetResource->save($set); + $groupId = $attributeGroup + ? $attributeGroup->getAttributeGroupId() + : $this->getAttributeGroup($groupName)->getAttributeGroupId(); + $this->config->clear(); + $setInfo = $this->attributeSetResource->getSetInfo([$attributeId], $this->defaultSetId); + $expectedInfo = [ + $attributeId => [$this->defaultSetId => ['group_id' => $groupId, 'group_sort' => '1', 'sort' => '1']], + ]; + $this->assertEquals($expectedInfo, $setInfo); + } + + /** + * @return array + */ + public function addAttributeToSetDataProvider(): array + { + return [ + 'add_to_existing_group' => [ + 'group_name' => 'Content', + 'attribute_code' => 'zzz', + ], + 'add_to_new_group' => [ + 'group_name' => 'Test', + 'attribute_code' => 'zzz', + ], + 'move_to_existing_group' => [ + 'group_name' => 'Images', + 'attribute_code' => 'description', + ], + 'move_to_new_group' => [ + 'group_name' => 'Test', + 'attribute_code' => 'description', + ], + ]; + } + + /** + * @return void + */ + public function testSaveWithChangedGroupSorting(): void + { + $set = $this->setRepository->get($this->defaultSetId); + $contentGroupId = $this->getAttributeGroup('Content')->getAttributeGroupId(); + $imagesGroupId = $this->getAttributeGroup('Images')->getAttributeGroupId(); + $additional = [ + 'groups' => [ + [$contentGroupId, 'Content', 2], + [$imagesGroupId, 'Images', 1] + ] + ]; + $set->organizeData($this->getAttributeSetData($additional)); + $this->attributeSetResource->save($set); + $contentGroupSort = $this->getAttributeGroup('Content')->getSortOrder(); + $imagesGroupSort = $this->getAttributeGroup('Images')->getSortOrder(); + $this->assertEquals(2, $contentGroupSort); + $this->assertEquals(1, $imagesGroupSort); + } + + /** + * @return void + */ + public function testSaveWithRemovedGroup(): void + { + $set = $this->setRepository->get($this->defaultSetId); + $designGroupId = $this->getAttributeGroup('Design')->getAttributeGroupId(); + $additional = [ + 'removeGroups' => [$designGroupId], + ]; + $set->organizeData($this->getAttributeSetData($additional)); + $this->attributeSetResource->save($set); + $this->assertNull( + $this->getAttributeGroup('Design'), + 'Group "Design" wan\'t deleted.' + ); + $unusedSetAttributes = $this->getSetExcludedAttributes((int)$set->getAttributeSetId()); + $designAttributeCodes = ['page_layout', 'options_container', 'custom_layout_update']; + $this->assertNotEmpty( + array_intersect($designAttributeCodes, $unusedSetAttributes), + 'Attributes from "Design" group still assigned to attribute set.' + ); + } + + /** + * @return void + */ + public function testSaveWithRemovedAttribute(): void + { + $set = $this->setRepository->get($this->defaultSetId); + $attributeId = (int)$this->attributeRepository->get('meta_description')->getAttributeId(); + $additional = [ + 'not_attributes' => [$this->getEntityAttributeId($this->defaultSetId, $attributeId)], + ]; + $set->organizeData($this->getAttributeSetData($additional)); + $this->attributeSetResource->save($set); + $this->config->clear(); + $setInfo = $this->attributeSetResource->getSetInfo([$attributeId], $this->defaultSetId); + $this->assertEmpty($setInfo[$attributeId]); + $unusedSetAttributes = $this->getSetExcludedAttributes((int)$set->getAttributeSetId()); + $this->assertNotEmpty( + array_intersect(['meta_description'], $unusedSetAttributes), + 'Attribute still assigned to attribute set.' + ); + } + + /** + * Returns attribute set data for saving. + * + * @param array $additional + * @return array + */ + private function getAttributeSetData(array $additional): array + { + $data = [ + 'attributes' => [], + 'groups' => [], + 'not_attributes' => [], + 'removeGroups' => [], + 'attribute_set_name' => 'Default', + ]; + + return array_merge($data, $additional); + } + + /** + * Returns attribute group by name. + * + * @param string $groupName + * @return AttributeGroupInterface|null + */ + private function getAttributeGroup(string $groupName): ?AttributeGroupInterface + { + return $this->attributeGroupByName->execute($this->defaultSetId, $groupName); + } + + /** + * Returns list of unused attributes in attribute set. + * + * @param int $setId + * @return array + */ + private function getSetExcludedAttributes(int $setId): array + { + $collection = $this->attributeCollectionFactory->create() + ->setExcludeSetFilter($setId); + $result = $collection->getColumnValues(AttributeInterface::ATTRIBUTE_CODE); + + return $result; + } + + /** + * Returns entity attribute id. + * + * @param int $setId + * @param int $attributeId + * @return int + */ + private function getEntityAttributeId(int $setId, int $attributeId): int + { + return $this->getEntityIdByAttributeId->execute($setId, $attributeId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Compare/ListCompareTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Compare/ListCompareTest.php index 3fa02aebe9170..98b264a8991bc 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Compare/ListCompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Compare/ListCompareTest.php @@ -6,10 +6,6 @@ namespace Magento\Catalog\Model\Product\Compare; -/** - * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDataFixture Magento/Customer/_files/customer.php - */ class ListCompareTest extends \PHPUnit\Framework\TestCase { /** @@ -44,6 +40,10 @@ protected function tearDown() $this->_session->setCustomerId(null); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ public function testAddProductWithSession() { $this->_session->setCustomerId(1); @@ -51,10 +51,33 @@ public function testAddProductWithSession() $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\Product::class) ->load(1); - $this->_model->addProduct($product); + /** @var $product2 \Magento\Catalog\Model\Product */ + $product2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\Product::class) + ->load(6); + $products = [$product->getId(), $product2->getId()]; + $this->_model->addProducts($products); + $this->assertTrue($this->_model->hasItems(1, $this->_visitor->getId())); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testAddProductWithSessionNeg() + { + $this->_session->setCustomerId(1); + $products = ['none', 99]; + $this->_model->addProducts($products); + + $this->assertFalse($this->_model->hasItems(1, $this->_visitor->getId())); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ public function testAddProductWithoutSession() { /** @var $product \Magento\Catalog\Model\Product */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CopierTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CopierTest.php new file mode 100644 index 0000000000000..6510e048f0e2d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CopierTest.php @@ -0,0 +1,47 @@ +get(ProductRepository::class); + $copier = Bootstrap::getObjectManager()->get(Copier::class); + + $product = $productRepository->get($productSKU); + $duplicate = $copier->copy($product); + + $duplicateStoreView = $productRepository->getById($duplicate->getId(), false, Store::DISTRO_STORE_ID); + $productStoreView = $productRepository->get($productSKU, false, Store::DISTRO_STORE_ID); + + $this->assertNotEquals( + $duplicateStoreView->getUrlKey(), + $productStoreView->getUrlKey(), + 'url_key of product duplicate should be different then url_key of the product for the same store view' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CreateCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CreateCustomOptionsTest.php new file mode 100644 index 0000000000000..2239170cdc84e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CreateCustomOptionsTest.php @@ -0,0 +1,929 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->optionRepository = $this->objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->customOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); + $this->customOptionValueFactory = $this->objectManager + ->get(ProductCustomOptionValuesInterfaceFactory::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + } + + /** + * Test to save option price by store. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_options.php + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoAppArea adminhtml + * @magentoAppIsolation disabled + * @magentoConfigFixture default_store catalog/price/scope 1 + * @magentoConfigFixture secondstore_store catalog/price/scope 1 + */ + public function testSaveOptionPriceByStore(): void + { + $secondWebsitePrice = 22.0; + $currentStoreId = $this->storeManager->getStore()->getId(); + $customStoreId = $this->storeManager->getStore('secondstore')->getId(); + $product = $this->productRepository->get('simple'); + $option = $product->getOptions()[0]; + $defaultPrice = $option->getPrice(); + $option->setPrice($secondWebsitePrice); + $product->setStoreId($customStoreId); + // set Current store='secondstore' to correctly save product options for 'secondstore' + try { + $this->storeManager->setCurrentStore($customStoreId); + $this->productRepository->save($product); + } finally { + $this->storeManager->setCurrentStore($currentStoreId); + } + $product = $this->productRepository->get('simple', false, $currentStoreId, true); + $option = $product->getOptions()[0]; + $this->assertEquals($defaultPrice, $option->getPrice(), 'Price value by default store is wrong'); + $product = $this->productRepository->get('simple', false, $customStoreId, true); + $option = $product->getOptions()[0]; + $this->assertEquals($secondWebsitePrice, $option->getPrice(), 'Price value by custom store is wrong'); + } + + /** + * Test add to product custom options with text type. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsTypeTextDataProvider + * + * @param array $optionData + */ + public function testCreateOptionsWithTypeText(array $optionData): void + { + $option = $this->baseCreateCustomOptionAndAssert($optionData); + $this->assertEquals($optionData['price'], $option->getPrice()); + $this->assertEquals($optionData['price_type'], $option->getPriceType()); + $this->assertEquals($optionData['sku'], $option->getSku()); + $maxCharacters = $optionData['max_characters'] ?? 0; + $this->assertEquals($maxCharacters, $option->getMaxCharacters()); + } + + /** + * Tests removing ineligible characters from file_extension. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider fileExtensionsDataProvider + * + * @param string $rawExtensions + * @param string $expectedExtensions + */ + public function testFileExtensions(string $rawExtensions, string $expectedExtensions): void + { + $product = $this->productRepository->get('simple'); + $optionData = [ + 'title' => 'file option', + 'type' => 'file', + 'is_require' => true, + 'sort_order' => 3, + 'price' => 30.0, + 'price_type' => 'percent', + 'sku' => 'sku3', + 'file_extension' => $rawExtensions, + 'image_size_x' => 10, + 'image_size_y' => 20, + ]; + $fileOption = $this->customOptionFactory->create(['data' => $optionData]); + $product->addOption($fileOption); + $this->productRepository->save($product); + $product = $this->productRepository->get('simple'); + $fileOption = $product->getOptions()[0]; + $actualExtensions = $fileOption->getFileExtension(); + $this->assertEquals($expectedExtensions, $actualExtensions); + } + + /** + * Test add to product custom options with select type. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsTypeSelectDataProvider + * + * @param array $optionData + * @param array $optionValueData + */ + public function testCreateOptionsWithTypeSelect(array $optionData, array $optionValueData): void + { + $optionValue = $this->customOptionValueFactory->create(['data' => $optionValueData]); + $optionData['values'] = [$optionValue]; + $option = $this->baseCreateCustomOptionAndAssert($optionData); + $optionValues = $option->getValues(); + $this->assertCount(1, $optionValues); + $this->assertNotNull($optionValues); + $optionValue = reset($optionValues); + $this->assertEquals($optionValueData['title'], $optionValue->getTitle()); + $this->assertEquals($optionValueData['price'], $optionValue->getPrice()); + $this->assertEquals($optionValueData['price_type'], $optionValue->getPriceType()); + $this->assertEquals($optionValueData['sku'], $optionValue->getSku()); + $this->assertEquals($optionValueData['sort_order'], $optionValue->getSortOrder()); + } + + /** + * Test add to product custom options with date type. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsTypeDateDataProvider + * + * @param array $optionData + */ + public function testCreateOptionsWithTypeDate(array $optionData): void + { + $option = $this->baseCreateCustomOptionAndAssert($optionData); + $this->assertEquals($optionData['price'], $option->getPrice()); + $this->assertEquals($optionData['price_type'], $option->getPriceType()); + $this->assertEquals($optionData['sku'], $option->getSku()); + } + + /** + * Check that error throws if we save porduct with custom option without some field. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsWithErrorDataProvider + * + * @param array $optionData + * @param \Exception $expectedErrorObject + */ + public function testCreateOptionWithError(array $optionData, \Exception $expectedErrorObject): void + { + $product = $this->productRepository->get('simple'); + $createdOption = $this->customOptionFactory->create(['data' => $optionData]); + $product->setOptions([$createdOption]); + $this->expectExceptionObject($expectedErrorObject); + $this->productRepository->save($product); + } + + /** + * Add option to product with type text data provider. + * + * @return array + */ + public function productCustomOptionsTypeTextDataProvider(): array + { + return [ + 'area_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + 'area_field_options_with_max_charters_configuration' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_options_without_max_charters_configuration' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ]; + } + + /** + * Data provider for testFileExtensions. + * + * @return array + */ + public function fileExtensionsDataProvider(): array + { + return [ + ['JPG, PNG, GIF', 'jpg, png, gif'], + ['jpg, jpg, jpg', 'jpg'], + ['jpg, png, gif', 'jpg, png, gif'], + ['jpg png gif', 'jpg, png, gif'], + ['!jpg@png#gif%', 'jpg, png, gif'], + ['jpg, png, 123', 'jpg, png, 123'], + ['', ''], + ]; + } + + /** + * Add option to product with type text data provider. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productCustomOptionsTypeSelectDataProvider(): array + { + return [ + 'drop_down_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'drop_down_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'drop_down_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'drop_down_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + ]; + } + + /** + * Add option to product with type text data provider. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productCustomOptionsTypeDateDataProvider(): array + { + return [ + 'date_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + 'date_time_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_time_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_time_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_time_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + 'time_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'time_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'time_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'time_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + ]; + } + + /** + * Add option to product for get option save error data provider. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productCustomOptionsWithErrorDataProvider(): array + { + return [ + 'error_option_without_product_sku' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + new CouldNotSaveException(__('The ProductSku is empty. Set the ProductSku and try again.')), + ], + 'error_option_without_type' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'price' => 10, + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__("Missed values for option required fields\nInvalid option type")), + ], + 'error_option_wrong_price_type' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'test_wrong_price_type', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Invalid option value')), + ], + 'error_option_without_price_type' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'product_sku' => 'simple', + ], + new ValidatorException(__('Invalid option value')), + ], + 'error_option_without_price_value' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Invalid option value')), + ], + 'error_option_without_title' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Missed values for option required fields')), + ], + 'error_option_with_empty_title' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => '', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Missed values for option required fields')), + ], + ]; + } + + /** + * Create custom option and save product with created option, check base assertions. + * + * @param array $optionData + * @return ProductCustomOptionInterface + */ + private function baseCreateCustomOptionAndAssert(array $optionData): ProductCustomOptionInterface + { + $product = $this->productRepository->get('simple'); + $createdOption = $this->customOptionFactory->create(['data' => $optionData]); + $createdOption->setProductSku($product->getSku()); + $product->setOptions([$createdOption]); + $this->productRepository->save($product); + $productCustomOptions = $this->optionRepository->getProductOptions($product); + $this->assertCount(1, $productCustomOptions); + $option = reset($productCustomOptions); + $this->assertEquals($optionData['title'], $option->getTitle()); + $this->assertEquals($optionData['type'], $option->getType()); + $this->assertEquals($optionData['is_require'], $option->getIsRequire()); + + return $option; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/DeleteCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/DeleteCustomOptionsTest.php new file mode 100644 index 0000000000000..8039d3515e304 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/DeleteCustomOptionsTest.php @@ -0,0 +1,238 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->optionRepository = $this->objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->customOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); + $this->customOptionValueFactory = $this->objectManager + ->get(ProductCustomOptionValuesInterfaceFactory::class); + + parent::setUp(); + } + + /** + * Test delete product custom options with type "area". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Area::getDataForCreateOptions() + * + * @param array $optionData + * @return void + */ + public function testDeleteAreaCustomOption(array $optionData): void + { + $this->deleteAndAssertNotSelectCustomOptions($optionData); + } + + /** + * Test delete product custom options with type "file". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\File::getDataForCreateOptions() + * + * @param array $optionData + * @return void + */ + public function testDeleteFileCustomOption(array $optionData): void + { + $this->deleteAndAssertNotSelectCustomOptions($optionData); + } + + /** + * Test delete product custom options with type "Date". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Date::getDataForCreateOptions() + * + * @param array $optionData + * @return void + */ + public function testDeleteDateCustomOption(array $optionData): void + { + $this->deleteAndAssertNotSelectCustomOptions($optionData); + } + + /** + * Test delete product custom options with type "Date & Time". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\DateTime::getDataForCreateOptions() + * + * @param array $optionData + * @return void + */ + public function testDeleteDateTimeCustomOption(array $optionData): void + { + $this->deleteAndAssertNotSelectCustomOptions($optionData); + } + + /** + * Test delete product custom options with type "Time". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Time::getDataForCreateOptions() + * + * @param array $optionData + * @return void + */ + public function testDeleteTimeCustomOption(array $optionData): void + { + $this->deleteAndAssertNotSelectCustomOptions($optionData); + } + + /** + * Test delete product custom options with type "Drop-down". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\DropDown::getDataForCreateOptions() + * + * @param array $optionData + * @param array $optionValueData + * @return void + */ + public function testDeleteDropDownCustomOption(array $optionData, array $optionValueData): void + { + $this->deleteAndAssertSelectCustomOptions($optionData, $optionValueData); + } + + /** + * Test delete product custom options with type "Radio Buttons". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\RadioButtons::getDataForCreateOptions() + * + * @param array $optionData + * @param array $optionValueData + * @return void + */ + public function testDeleteRadioButtonsCustomOption(array $optionData, array $optionValueData): void + { + $this->deleteAndAssertSelectCustomOptions($optionData, $optionValueData); + } + + /** + * Test delete product custom options with type "Checkbox". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Checkbox::getDataForCreateOptions() + * + * @param array $optionData + * @param array $optionValueData + * @return void + */ + public function testDeleteCheckboxCustomOption(array $optionData, array $optionValueData): void + { + $this->deleteAndAssertSelectCustomOptions($optionData, $optionValueData); + } + + /** + * Test delete product custom options with type "Multiple Select". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\MultipleSelect::getDataForCreateOptions() + * + * @param array $optionData + * @param array $optionValueData + * @return void + */ + public function testDeleteMultipleSelectCustomOption(array $optionData, array $optionValueData): void + { + $this->deleteAndAssertSelectCustomOptions($optionData, $optionValueData); + } + + /** + * Delete product custom options which are not from "select" group and assert that option was deleted. + * + * @param array $optionData + * @return void + */ + private function deleteAndAssertNotSelectCustomOptions(array $optionData): void + { + $product = $this->productRepository->get('simple'); + $createdOption = $this->customOptionFactory->create(['data' => $optionData]); + $createdOption->setProductSku($product->getSku()); + $product->setOptions([$createdOption]); + $this->productRepository->save($product); + $this->assertCount(1, $this->optionRepository->getProductOptions($product)); + $product->setOptions([]); + $this->productRepository->save($product); + $this->assertCount(0, $this->optionRepository->getProductOptions($product)); + } + + /** + * Delete product custom options which from "select" group and assert that option was deleted. + * + * @param array $optionData + * @param array $optionValueData + * @return void + */ + private function deleteAndAssertSelectCustomOptions(array $optionData, array $optionValueData): void + { + $optionValue = $this->customOptionValueFactory->create(['data' => $optionValueData]); + $optionData['values'] = [$optionValue]; + $this->deleteAndAssertNotSelectCustomOptions($optionData); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php index 7421402455b28..2277470e33b12 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php @@ -3,50 +3,87 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Gallery; -use Magento\Framework\Exception\FileSystemException; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; /** - * Test class for \Magento\Catalog\Model\Product\Gallery\CreateHandler. + * Provides tests for media gallery images creation during product save. * * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @magentoDbIsolation enabled */ class CreateHandlerTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Product\Gallery\CreateHandler + * @var string */ - protected $createHandler; - private $fileName = '/m/a/magento_image.jpg'; + /** + * @var string + */ private $fileLabel = 'Magento image'; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CreateHandler + */ + private $createHandler; + + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductResource + */ + private $productResource; + + /** + * @inheritdoc + */ protected function setUp() { - $this->createHandler = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product\Gallery\CreateHandler::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->createHandler = $this->objectManager->create(CreateHandler::class); + $this->galleryResource = $this->objectManager->create(Gallery::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); } /** + * Tests gallery processing on product duplication. + * * @covers \Magento\Catalog\Model\Product\Gallery\CreateHandler::execute + * + * @return void */ - public function testExecuteWithImageDuplicate() + public function testExecuteWithImageDuplicate(): void { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $this->fileName, 'label' => $this->fileLabel]]] - ); - $product->setData('image', $this->fileName); + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => $this->fileLabel]]], + 'image' => $this->fileName, + ]; + $product = $this->initProduct($data); $this->createHandler->execute($product); $this->assertStringStartsWith('/m/a/magento_image', $product->getData('media_gallery/images/image/new_file')); $this->assertEquals($this->fileLabel, $product->getData('image_label')); @@ -62,39 +99,29 @@ public function testExecuteWithImageDuplicate() } /** - * Check sanity of posted image file name + * Check sanity of posted image file name. * * @param string $imageFileName - * @throws FileSystemException * @expectedException \Magento\Framework\Exception\FileSystemException + * @expectedExceptionMessageRegExp ".+ file doesn't exist." + * @expectedExceptionMessageRegExp "/^((?!\.\.\/).)*$/" * @dataProvider illegalFilenameDataProvider + * @return void */ - public function testExecuteWithIllegalFilename($imageFileName) + public function testExecuteWithIllegalFilename(string $imageFileName): void { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $imageFileName, 'label' => 'New image']]] - ); + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $imageFileName, 'label' => 'New image']]], + ]; + $product = $this->initProduct($data); $product->setData('image', $imageFileName); - - try { - $this->createHandler->execute($product); - } catch (FileSystemException $exception) { - $this->assertContains(" file doesn't exist.", $exception->getLogMessage()); - $this->assertNotContains('../', $exception->getLogMessage()); - throw $exception; - } + $this->createHandler->execute($product); } /** * @return array */ - public function illegalFilenameDataProvider() + public function illegalFilenameDataProvider(): array { return [ ['../../../../../.htaccess'], @@ -103,123 +130,196 @@ public function illegalFilenameDataProvider() } /** + * Tests gallery processing with different image roles. + * * @dataProvider executeDataProvider - * @param $image - * @param $smallImage - * @param $swatchImage - * @param $thumbnail + * @param string $image + * @param string $smallImage + * @param string $swatchImage + * @param string $thumbnail + * @return void */ - public function testExecuteWithImageRoles($image, $smallImage, $swatchImage, $thumbnail) - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]] - ); - $product->setData('image', $image); - $product->setData('small_image', $smallImage); - $product->setData('swatch_image', $swatchImage); - $product->setData('thumbnail', $thumbnail); + public function testExecuteWithImageRoles( + string $image, + string $smallImage, + string $swatchImage, + string $thumbnail + ): void { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]], + 'image' => $image, + 'small_image' => $smallImage, + 'swatch_image' => $swatchImage, + 'thumbnail' => $thumbnail, + ]; + $product = $this->initProduct($data); $this->createHandler->execute($product); - - $resource = $product->getResource(); - $id = $product->getId(); - $storeId = $product->getStoreId(); - - $this->assertStringStartsWith('/m/a/magento_image', $product->getData('media_gallery/images/image/new_file')); - $this->assertEquals( - $image, - $resource->getAttributeRawValue($id, $resource->getAttribute('image'), $storeId) - ); - $this->assertEquals( - $smallImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('small_image'), $storeId) - ); - $this->assertEquals( - $swatchImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('swatch_image'), $storeId) - ); - $this->assertEquals( - $thumbnail, - $resource->getAttributeRawValue($id, $resource->getAttribute('thumbnail'), $storeId) - ); + $this->assertMediaImageRoleAttributes($product, $image, $smallImage, $swatchImage, $thumbnail); } /** + * Tests gallery processing without images. + * * @dataProvider executeDataProvider - * @param $image - * @param $smallImage - * @param $swatchImage - * @param $thumbnail + * @param string $image + * @param string $smallImage + * @param string $swatchImage + * @param string $thumbnail + * @return void */ - public function testExecuteWithoutImages($image, $smallImage, $swatchImage, $thumbnail) - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]] - ); - $product->setData('image', $image); - $product->setData('small_image', $smallImage); - $product->setData('swatch_image', $swatchImage); - $product->setData('thumbnail', $thumbnail); + public function testExecuteWithoutImages( + string $image, + string $smallImage, + string $swatchImage, + string $thumbnail + ): void { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]], + 'image' => $image, + 'small_image' => $smallImage, + 'swatch_image' => $swatchImage, + 'thumbnail' => $thumbnail, + ]; + $product = $this->initProduct($data); $this->createHandler->execute($product); - $product->unsetData('image'); $product->unsetData('small_image'); $product->unsetData('swatch_image'); $product->unsetData('thumbnail'); $this->createHandler->execute($product); - - $resource = $product->getResource(); - $id = $product->getId(); - $storeId = $product->getStoreId(); - - $this->assertStringStartsWith('/m/a/magento_image', $product->getData('media_gallery/images/image/new_file')); - $this->assertEquals( - $image, - $resource->getAttributeRawValue($id, $resource->getAttribute('image'), $storeId) - ); - $this->assertEquals( - $smallImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('small_image'), $storeId) - ); - $this->assertEquals( - $swatchImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('swatch_image'), $storeId) - ); - $this->assertEquals( - $thumbnail, - $resource->getAttributeRawValue($id, $resource->getAttribute('thumbnail'), $storeId) - ); + $this->assertMediaImageRoleAttributes($product, $image, $smallImage, $swatchImage, $thumbnail); } /** * @return array */ - public function executeDataProvider() + public function executeDataProvider(): array { return [ [ 'image' => $this->fileName, 'small_image' => $this->fileName, 'swatch_image' => $this->fileName, - 'thumbnail' => $this->fileName + 'thumbnail' => $this->fileName, ], [ 'image' => 'no_selection', 'small_image' => 'no_selection', 'swatch_image' => 'no_selection', - 'thumbnail' => 'no_selection' - ] + 'thumbnail' => 'no_selection', + ], + ]; + } + + /** + * Tests gallery processing with variations of additional gallery image fields. + * + * @dataProvider additionalGalleryFieldsProvider + * @param string $mediaField + * @param string $value + * @param string|null $expectedValue + * @return void + */ + public function testExecuteWithAdditionalGalleryFields( + string $mediaField, + string $value, + ?string $expectedValue + ): void { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, $mediaField => $value]]], ]; + $product = $this->initProduct($data); + $this->createHandler->execute($product); + $galleryAttributeId = $this->productResource->getAttribute('media_gallery')->getAttributeId(); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $galleryAttributeId); + $image = reset($productImages); + $this->assertEquals($image[$mediaField], $expectedValue); + } + + /** + * @return array + */ + public function additionalGalleryFieldsProvider(): array + { + return [ + ['label', '', null], + ['label', 'Some label', 'Some label'], + ['disabled', '0', '0'], + ['disabled', '1', '1'], + ['position', '1', '1'], + ['position', '2', '2'], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_image_attribute.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @return void + */ + public function testExecuteWithCustomMediaAttribute(): void + { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]], + 'image' => 'no_selection', + 'small_image' => 'no_selection', + 'swatch_image' => 'no_selection', + 'thumbnail' => 'no_selection', + 'image_attribute' => $this->fileName + ]; + $product = $this->initProduct($data); + $this->createHandler->execute($product); + $mediaAttributeValue = $this->productResource->getAttributeRawValue( + $product->getId(), + ['image_attribute'], + $product->getStoreId() + ); + $this->assertEquals($this->fileName, $mediaAttributeValue); + } + + /** + * Returns product for testing. + * + * @param array $data + * @return Product + */ + private function initProduct(array $data): Product + { + $product = $this->productRepository->getById(1); + $product->addData($data); + + return $product; + } + + /** + * Asserts product attributes related to gallery images. + * + * @param Product $product + * @param string $image + * @param string $smallImage + * @param string $swatchImage + * @param string $thumbnail + * @return void + */ + private function assertMediaImageRoleAttributes( + Product $product, + string $image, + string $smallImage, + string $swatchImage, + string $thumbnail + ): void { + $productsImageData = $this->productResource->getAttributeRawValue( + $product->getId(), + ['image', 'small_image', 'thumbnail', 'swatch_image'], + $product->getStoreId() + ); + $this->assertStringStartsWith( + '/m/a/magento_image', + $product->getData('media_gallery/images/image/new_file') + ); + $this->assertEquals($image, $productsImageData['image']); + $this->assertEquals($smallImage, $productsImageData['small_image']); + $this->assertEquals($swatchImage, $productsImageData['swatch_image']); + $this->assertEquals($thumbnail, $productsImageData['thumbnail']); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ReadHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ReadHandlerTest.php index 35c995ec552a5..89b91ab57e51a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ReadHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ReadHandlerTest.php @@ -3,75 +3,350 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Gallery; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; /** - * Test class for \Magento\Catalog\Model\Product\Gallery\ReadHandler. + * Provide tests for loading gallery images on product load. * - * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ReadHandlerTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\TestFramework\ObjectManager + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ReadHandler + */ + private $readHandler; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductInterfaceFactory */ - protected $objectManager; + private $productFactory; /** - * @var \Magento\Catalog\Model\Product\Gallery\ReadHandler + * @var ProductResource */ - protected $readHandler; + private $productResource; + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var string + */ + private $productLinkField; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - - $this->readHandler = $this->objectManager->create( - \Magento\Catalog\Model\Product\Gallery\ReadHandler::class - ); + $this->readHandler = $this->objectManager->create(ReadHandler::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->productResource = $this->objectManager->get(ProductResource::class); + $this->galleryResource = $this->objectManager->create(Gallery::class); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + $this->productLinkField = $this->objectManager->get(MetadataPool::class) + ->getMetadata(ProductInterface::class) + ->getLinkField(); } /** - * @covers \Magento\Catalog\Model\Product\Gallery\ReadHandler::execute + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation enabled + * @return void */ - public function testExecute() + public function testExecuteWithoutImages(): void { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create( - \Magento\Catalog\Model\Product::class - ); - - /** - * @var $entityMetadata \Magento\Framework\EntityManager\EntityMetadata - */ - $entityMetadata = $this->objectManager - ->get(MetadataPool::class) - ->getMetadata(ProductInterface::class); - $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $linkFieldId = $productRepository->get('simple')->getData($entityMetadata->getLinkField()); - - $product->setData($entityMetadata->getLinkField(), $linkFieldId); + $product = $this->getProductInstance(); $this->readHandler->execute($product); - $data = $product->getData(); - $this->assertArrayHasKey('media_gallery', $data); $this->assertArrayHasKey('images', $data['media_gallery']); + $this->assertCount(0, $data['media_gallery']['images']); + } + /** + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDbIsolation enabled + * @return void + */ + public function testExecuteWithOneImage(): void + { + $product = $this->getProductInstance(); + $this->readHandler->execute($product); + $data = $product->getData(); + $this->assertArrayHasKey('media_gallery', $data); + $this->assertArrayHasKey('images', $data['media_gallery']); $this->assertCount(1, $data['media_gallery']['images']); + $galleryImage = reset($data['media_gallery']['images']); + $this->assertEquals('/m/a/magento_image.jpg', $galleryImage['file']); + $this->assertEquals(1, $galleryImage['position']); + $this->assertEquals('Image Alt Text', $galleryImage['label']); + $this->assertEquals(0, $galleryImage['disabled']); + } + + /** + * @dataProvider executeWithTwoImagesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDbIsolation enabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testExecuteWithTwoImages(array $images, array $expectation): void + { + $this->setGalleryImages($this->getProduct(), $images); + $productInstance = $this->getProductInstance(); + $this->readHandler->execute($productInstance); + $data = $productInstance->getData(); + $this->assertArrayHasKey('media_gallery', $data); + $this->assertArrayHasKey('images', $data['media_gallery']); + $this->assertCount(count($expectation), $data['media_gallery']['images']); + $imagesToAssert = []; foreach ($data['media_gallery']['images'] as $valueId => $imageData) { - $this->assertEquals( - 'Image Alt Text', - $imageData['label'] - ); + $imagesToAssert[] = [ + 'file' => $imageData['file'], + 'label' => $imageData['label'], + 'position' => $imageData['position'], + 'disabled' => $imageData['disabled'], + ]; $this->assertEquals( $imageData['value_id'], $valueId ); } + $this->assertEquals($expectation, $imagesToAssert); + } + + /** + * @return array + */ + public function executeWithTwoImagesDataProvider(): array + { + return [ + 'with_two_images' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => '1', + 'disabled' => '0', + ], + [ + 'file' => '/m/a/magento_thumbnail.jpg', + 'label' => 'Thumbnail Image', + 'position' => '2', + 'disabled' => '0', + ], + ], + ], + 'with_two_images_and_changed_position_and_one_disabled' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [ + 'position' => '2', + 'disabled' => '0', + ], + '/m/a/magento_thumbnail.jpg' => [ + 'position' => '1', + 'disabled' => '1', + ], + ], + 'expectation' => [ + [ + 'file' => '/m/a/magento_thumbnail.jpg', + 'label' => 'Thumbnail Image', + 'position' => '1', + 'disabled' => '1', + ], + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => '2', + 'disabled' => '0', + ], + ], + ], + ]; + } + + /** + * @dataProvider executeOnStoreViewDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDbIsolation disabled + * @param string $file + * @param string $field + * @param string $value + * @param array $expectation + * @return void + */ + public function testExecuteOnStoreView(string $file, string $field, string $value, array $expectation): void + { + $product = $this->getProduct(); + $secondStoreId = (int)$this->storeRepository->get('fixture_second_store')->getId(); + $this->setGalleryImages($product, [$file => [$field => $value]], (int)$secondStoreId); + $productInstance = $this->getProductInstance($secondStoreId); + $this->readHandler->execute($productInstance); + $data = $productInstance->getData(); + $this->assertArrayHasKey('media_gallery', $data); + $this->assertArrayHasKey('images', $data['media_gallery']); + $image = reset($data['media_gallery']['images']); + $dataToAssert = [ + $field => $image[$field], + $field . '_default' => $image[$field . '_default'], + ]; + $this->assertEquals($expectation, $dataToAssert); + } + + /** + * @return array + */ + public function executeOnStoreViewDataProvider(): array + { + return [ + 'with_store_label' => [ + 'file' => '/m/a/magento_image.jpg', + 'field' => 'label', + 'value' => 'Some store label', + 'expectation' => [ + 'label' => 'Some store label', + 'label_default' => 'Image Alt Text', + ], + ], + 'with_store_position' => [ + 'file' => '/m/a/magento_image.jpg', + 'field' => 'position', + 'value' => '2', + 'expectation' => [ + 'position' => '2', + 'position_default' => '1', + ], + ], + 'with_store_disabled' => [ + 'file' => '/m/a/magento_image.jpg', + 'field' => 'disabled', + 'value' => '1', + 'expectation' => [ + 'disabled' => '1', + 'disabled_default' => '0', + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + parent::tearDown(); + $this->galleryResource->getConnection() + ->delete($this->galleryResource->getTable(Gallery::GALLERY_TABLE)); + $this->galleryResource->getConnection() + ->delete($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE)); + $this->galleryResource->getConnection() + ->delete($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE)); + } + + /** + * Returns product for testing. + * + * @return ProductInterface + */ + private function getProduct(): ProductInterface + { + return $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID); + } + + /** + * Updates product gallery images and saves product. + * + * @param ProductInterface $product + * @param array $images + * @param int|null $storeId + * @return void + */ + private function setGalleryImages(ProductInterface $product, array $images, ?int $storeId = null): void + { + $product->setImage(null); + foreach ($images as $file => $data) { + $mediaGalleryData = $product->getData('media_gallery'); + foreach ($mediaGalleryData['images'] as &$image) { + if ($image['file'] == $file) { + foreach ($data as $key => $value) { + $image[$key] = $value; + } + } + } + + $product->setData('media_gallery', $mediaGalleryData); + if (!empty($data['main'])) { + $product->setImage($file); + } + } + + if ($storeId) { + $product->setStoreId($storeId); + } + + $this->productResource->save($product); + } + + /** + * Returns empty product instance. + * + * @param int|null $storeId + * @return ProductInterface + */ + private function getProductInstance(?int $storeId = null): ProductInterface + { + /** @var ProductInterface $product */ + $product = $this->productFactory->create(); + $product->setData( + $this->productLinkField, + $this->getProduct()->getData($this->productLinkField) + ); + + if ($storeId) { + $product->setStoreId($storeId); + } + + return $product; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 559dd6d1b747d..fcee06187f374 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -8,18 +8,24 @@ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; -use Magento\Framework\Filesystem; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\Filesystem\Directory\WriteInterface; /** - * Test for \Magento\Catalog\Model\Product\Gallery\UpdateHandler. + * Provides tests for media gallery images update during product save. * - * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase { @@ -33,68 +39,342 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase */ private $updateHandler; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var ProductResource + */ + private $productResource; + /** * @var WriteInterface */ private $mediaDirectory; /** - * @var Filesystem + * @var Config */ - private $filesystem; + private $config; /** * @var string */ private $fileName; + /** + * @var int + */ + private $mediaAttributeId; + /** * @inheritdoc */ protected function setUp() { $this->fileName = 'image.txt'; - $this->objectManager = Bootstrap::getObjectManager(); $this->updateHandler = $this->objectManager->create(UpdateHandler::class); - $this->filesystem = $this->objectManager->get(Filesystem::class); - $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + $this->galleryResource = $this->objectManager->create(Gallery::class); + $this->productResource = $this->objectManager->create(ProductResource::class); + $this->mediaAttributeId = (int)$this->productResource->getAttribute('media_gallery')->getAttributeId(); + $this->config = $this->objectManager->get(Config::class); + $this->mediaDirectory = $this->objectManager->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); $this->mediaDirectory->writeFile($this->fileName, 'Test'); } /** + * Tests updating image with illegal filename during product save. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_image.php + * * @return void */ public function testExecuteWithIllegalFilename(): void { - $filePath = str_repeat('/..', 2) . DIRECTORY_SEPARATOR . $this->fileName; - - /** @var $product Product */ - $product = Bootstrap::getObjectManager()->create(Product::class); - $product->load(1); + $product = $this->getProduct(); $product->setData( 'media_gallery', [ 'images' => [ 'image' => [ 'value_id' => '100', - 'file' => $filePath, + 'file' => '/../..' . DIRECTORY_SEPARATOR . $this->fileName, 'label' => 'New image', 'removed' => 1, ], ], ] ); - $this->updateHandler->execute($product); $this->assertFileExists($this->mediaDirectory->getAbsolutePath($this->fileName)); } /** + * Tests updating image label, position and disabling during product save. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDbIsolation enabled * @return void */ - protected function tearDown(): void + public function testExecuteWithOneImage(): void { + $product = $this->getProduct(); + $this->updateProductGalleryImages($product, ['label' => 'New image', 'disabled' => '1']); + $this->updateHandler->execute($product); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); + $updatedImage = reset($productImages); + $this->assertTrue(is_array($updatedImage)); + $this->assertEquals('New image', $updatedImage['label']); + $this->assertEquals('New image', $updatedImage['label_default']); + $this->assertEquals('1', $updatedImage['disabled']); + $this->assertEquals('1', $updatedImage['disabled_default']); + } + + /** + * Tests updating image roles during product save. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @dataProvider executeWithTwoImagesAndRolesDataProvider + * @magentoDbIsolation enabled + * @param array $roles + * @return void + */ + public function testExecuteWithTwoImagesAndDifferentRoles(array $roles): void + { + $imageRoles = ['image', 'small_image', 'thumbnail', 'swatch_image']; + $product = $this->getProduct(); + $product->addData($roles); + $this->updateHandler->execute($product); + $productsImageData = $this->productResource->getAttributeRawValue( + $product->getId(), + $imageRoles, + $product->getStoreId() + ); + $this->assertEquals($roles, $productsImageData); + } + + /** + * Tests updating image roles during product save on non default store view. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider executeWithTwoImagesAndRolesDataProvider + * @magentoDbIsolation enabled + * @param array $roles + * @return void + */ + public function testExecuteWithTwoImagesAndDifferentRolesOnStoreView(array $roles): void + { + $secondStoreId = (int)$this->storeRepository->get('fixture_second_store')->getId(); + $imageRoles = ['image', 'small_image', 'thumbnail', 'swatch_image']; + $product = $this->getProduct($secondStoreId); + $product->addData($roles); + $this->updateHandler->execute($product); + + $storeImages = $this->productResource->getAttributeRawValue( + $product->getId(), + $imageRoles, + $secondStoreId + ); + $this->assertEquals($roles, $storeImages); + + $defaultImages = $this->productResource->getAttributeRawValue( + $product->getId(), + $imageRoles, + Store::DEFAULT_STORE_ID + ); + $this->assertEquals('/m/a/magento_image.jpg', $defaultImages['image']); + $this->assertEquals('/m/a/magento_image.jpg', $defaultImages['small_image']); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $defaultImages['thumbnail']); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $defaultImages['swatch_image']); + } + + /** + * @return array + */ + public function executeWithTwoImagesAndRolesDataProvider(): array + { + return [ + 'unassign_all_roles' => [ + 'roles' => [ + 'image' => 'no_selection', + 'small_image' =>'no_selection', + 'thumbnail' => 'no_selection', + 'swatch_image' => 'no_selection', + ], + ], + 'assign_already_used_role' => [ + 'roles' => [ + 'image' => '/m/a/magento_image.jpg', + 'small_image' => '/m/a/magento_thumbnail.jpg', + 'thumbnail' => '/m/a/magento_thumbnail.jpg', + 'swatch_image' => '/m/a/magento_image.jpg', + ], + ], + ]; + } + + /** + * Tests updating image position during product save. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDbIsolation enabled + * @return void + */ + public function testExecuteWithTwoImagesAndChangedPosition(): void + { + $positionMap = [ + '/m/a/magento_image.jpg' => '2', + '/m/a/magento_thumbnail.jpg' => '1', + ]; + $product = $this->getProduct(); + $images = $product->getData('media_gallery')['images']; + foreach ($images as &$image) { + $image['position'] = $positionMap[$image['file']]; + } + $product->setData('store_id', Store::DEFAULT_STORE_ID); + $product->setData('media_gallery', ['images' => $images]); + $this->updateHandler->execute($product); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); + foreach ($productImages as $updatedImage) { + $this->assertEquals($positionMap[$updatedImage['file']], $updatedImage['position']); + $this->assertEquals($positionMap[$updatedImage['file']], $updatedImage['position_default']); + } + } + + /** + * Tests image remove during product save. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDbIsolation enabled + * @return void + */ + public function testExecuteWithImageToDelete(): void + { + $product = $this->getProduct(); + $image = $product->getImage(); + $this->updateProductGalleryImages($product, ['removed' => '1']); + $this->updateHandler->execute($product); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); + $this->assertCount(0, $productImages); + $this->assertFileNotExists( + $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $image) + ); + $defaultImages = $this->productResource->getAttributeRawValue( + $product->getId(), + ['image', 'small_image', 'thumbnail', 'swatch_image'], + Store::DEFAULT_STORE_ID + ); + $this->assertEquals('no_selection', $defaultImages['image']); + $this->assertEquals('no_selection', $defaultImages['small_image']); + $this->assertEquals('no_selection', $defaultImages['thumbnail']); + } + + /** + * Tests updating images data during product save on non default store view. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDbIsolation enabled + * @return void + */ + public function testExecuteWithTwoImagesOnStoreView(): void + { + $secondStoreId = (int)$this->storeRepository->get('fixture_second_store')->getId(); + $storeImages = [ + '/m/a/magento_image.jpg' => [ + 'label' => 'Store image', + 'label_default' => 'Image Alt Text', + 'disabled' => '1', + 'disabled_default' => '0', + 'position' => '2', + 'position_default' => '1', + ], + '/m/a/magento_thumbnail.jpg' => [ + 'label' => 'Store thumbnail', + 'label_default' => 'Thumbnail Image', + 'disabled' => '0', + 'disabled_default' => '0', + 'position' => '1', + 'position_default' => '2', + ], + ]; + $product = $this->getProduct($secondStoreId); + $images = $product->getData('media_gallery')['images']; + foreach ($images as &$image) { + $image['label'] = $storeImages[$image['file']]['label']; + $image['disabled'] = $storeImages[$image['file']]['disabled']; + $image['position'] = $storeImages[$image['file']]['position']; + } + $product->setData('media_gallery', ['images' => $images]); + $this->updateHandler->execute($product); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); + foreach ($productImages as $image) { + $imageToAssert = [ + 'label' => $image['label'], + 'label_default' =>$image['label_default'], + 'disabled' =>$image['disabled'], + 'disabled_default' => $image['disabled_default'], + 'position' => $image['position'], + 'position_default' => $image['position_default'], + ]; + $this->assertEquals($storeImages[$image['file']], $imageToAssert); + } + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + parent::tearDown(); $this->mediaDirectory->getDriver()->deleteFile($this->mediaDirectory->getAbsolutePath($this->fileName)); + $this->galleryResource->getConnection() + ->delete($this->galleryResource->getTable(Gallery::GALLERY_TABLE)); + $this->galleryResource->getConnection() + ->delete($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE)); + $this->galleryResource->getConnection() + ->delete($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE)); + } + + /** + * Returns current product. + * + * @param int|null $storeId + * @return ProductInterface|Product + */ + private function getProduct(?int $storeId = null): ProductInterface + { + return $this->productRepository->get('simple', false, $storeId, true); + } + + /** + * @param ProductInterface|Product $product + * @param array $imageData + * @return void + */ + private function updateProductGalleryImages(ProductInterface $product, array $imageData): void + { + $images = $product->getData('media_gallery')['images']; + $image = reset($images) ?: []; + $product->setData('store_id', Store::DEFAULT_STORE_ID); + $product->setData('media_gallery', ['images' => ['image' => array_merge($image, $imageData)]]); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/LinksTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/LinksTest.php new file mode 100644 index 0000000000000..b8be34f460dcb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/LinksTest.php @@ -0,0 +1,267 @@ + '2', + 'sku' => 'custom-design-simple-product', + 'position' => 1, + ], + [ + 'id' => '10', + 'sku' => 'simple1', + 'position' => 2, + ], + ]; + + /** @var array */ + private $existingProducts = [ + [ + 'id' => '10', + 'sku' => 'simple1', + 'position' => 1, + ], + [ + 'id' => '11', + 'sku' => 'simple2', + 'position' => 2, + ], + [ + 'id' => '12', + 'sku' => 'simple3', + 'position' => 3, + ], + ]; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var ObjectManager */ + private $objectManager; + + /** @var ProductResource */ + private $productResource; + + /** @var ProductLinkInterfaceFactory */ + private $productLinkInterfaceFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->productResource = $this->objectManager->create(ProductResource::class); + $this->productLinkInterfaceFactory = $this->objectManager->create(ProductLinkInterfaceFactory::class); + } + + /** + * Test edit and remove simple related, up-sells, cross-sells products in an existing product + * + * @dataProvider editDeleteRelatedUpSellCrossSellProductsProvider + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @param array $data + * @return void + */ + public function testEditRemoveRelatedUpSellCrossSellProducts(array $data): void + { + /** @var ProductInterface|Product $product */ + $product = $this->productRepository->get('simple'); + $this->setCustomProductLinks($product, $this->getProductData($data['defaultLinks'])); + $this->productRepository->save($product); + + $productData = $this->getProductData($data['productLinks']); + $this->setCustomProductLinks($product, $productData); + $this->productResource->save($product); + + $product = $this->productRepository->get('simple'); + $expectedLinks = isset($data['expectedProductLinks']) + ? $this->getProductData($data['expectedProductLinks']) + : $productData; + + $this->assertEquals( + $expectedLinks, + $this->getActualLinks($product), + "Expected linked products do not match actual linked products!" + ); + } + + /** + * Provide test data for testEditDeleteRelatedUpSellCrossSellProducts(). + * + * @return array + */ + public function editDeleteRelatedUpSellCrossSellProductsProvider(): array + { + return [ + 'update' => [ + 'data' => [ + 'defaultLinks' => $this->defaultDataFixture, + 'productLinks' => $this->existingProducts, + ], + ], + 'delete' => [ + 'data' => [ + 'defaultLinks' => $this->defaultDataFixture, + 'productLinks' => [] + ], + ], + 'same' => [ + 'data' => [ + 'defaultLinks' => $this->existingProducts, + 'productLinks' => $this->existingProducts, + ], + ], + 'change_position' => [ + 'data' => [ + 'defaultLinks' => $this->existingProducts, + 'productLinks' => array_replace_recursive( + $this->existingProducts, + [ + ['position' => 4], + ['position' => 5], + ['position' => 6], + ] + ), + ], + ], + 'without_position' => [ + 'data' => [ + 'defaultLinks' => $this->defaultDataFixture, + 'productLinks' => array_replace_recursive( + $this->existingProducts, + [ + ['position' => null], + ['position' => null], + ['position' => null], + ] + ), + 'expectedProductLinks' => array_replace_recursive( + $this->existingProducts, + [ + ['position' => 1], + ['position' => 2], + ['position' => 3], + ] + ), + ], + ], + ]; + } + + /** + * Create an array of products by link type that will be linked + * + * @param array $productFixture + * @return array + */ + private function getProductData(array $productFixture): array + { + $productData = []; + foreach ($this->linkTypes as $linkType) { + $productData[$linkType] = []; + foreach ($productFixture as $data) { + $productData[$linkType][] = $data; + } + } + + return $productData; + } + + /** + * Link related, up-sells, cross-sells products received from the array + * + * @param ProductInterface|Product $product + * @param array $productData + * @return void + */ + private function setCustomProductLinks(ProductInterface $product, array $productData): void + { + $productLinks = []; + foreach ($productData as $linkType => $links) { + foreach ($links as $data) { + /** @var ProductLinkInterface|Link $productLink */ + $productLink = $this->productLinkInterfaceFactory->create(); + $productLink->setSku('simple'); + $productLink->setLinkedProductSku($data['sku']); + if (isset($data['position'])) { + $productLink->setPosition($data['position']); + } + $productLink->setLinkType($linkType); + $productLinks[] = $productLink; + } + } + $product->setProductLinks($productLinks); + } + + /** + * Get an array of received related, up-sells, cross-sells products + * + * @param ProductInterface|Product $product + * @return array + */ + private function getActualLinks(ProductInterface $product): array + { + $actualLinks = []; + foreach ($this->linkTypes as $linkType) { + $products = []; + $actualLinks[$linkType] = []; + switch ($linkType) { + case 'upsell': + $products = $product->getUpSellProducts(); + break; + case 'crosssell': + $products = $product->getCrossSellProducts(); + break; + case 'related': + $products = $product->getRelatedProducts(); + break; + } + /** @var ProductInterface|Product $productItem */ + foreach ($products as $productItem) { + $actualLinks[$linkType][] = [ + 'id' => $productItem->getId(), + 'sku' => $productItem->getSku(), + 'position' => $productItem->getPosition(), + ]; + } + } + + return $actualLinks; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php index a6538423f37a1..8463577c34ed9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php @@ -6,13 +6,16 @@ namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\DataObject; + /** - * Test for \Magento\Catalog\Model\Product\Option\Type\Date + * Test for customizable product option with "Date" type */ class DateTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Product\Option\Type\Date + * @var Date */ protected $model; @@ -28,12 +31,13 @@ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->model = $this->objectManager->create( - \Magento\Catalog\Model\Product\Option\Type\Date::class + Date::class ); } /** - * @covers \Magento\Catalog\Model\Product\Option\Type\Date::prepareOptionValueForRequest() + * Check if option value for request is the same as expected + * * @dataProvider prepareOptionValueForRequestDataProvider * @param array $optionValue * @param array $infoBuyRequest @@ -54,10 +58,10 @@ public function testPrepareOptionValueForRequest( /** @var \Magento\Quote\Model\Quote\Item $item */ $item = $this->objectManager->create(\Magento\Quote\Model\Quote\Item::class); $item->addOption($option); - /** @var \Magento\Catalog\Model\Product\Option|null $productOption */ + /** @var Option|null $productOption */ $productOption = $productOptionData ? $this->objectManager->create( - \Magento\Catalog\Model\Product\Option::class, + Option::class, ['data' => $productOptionData] ) : null; @@ -69,6 +73,8 @@ public function testPrepareOptionValueForRequest( } /** + * Data provider for testPrepareOptionValueForRequest + * * @return array */ public function prepareOptionValueForRequestDataProvider() @@ -109,4 +115,76 @@ public function prepareOptionValueForRequestDataProvider() ], ]; } + + /** + * Check date in prepareForCart method with javascript calendar and Asia/Singapore timezone + * + * @dataProvider testPrepareForCartDataProvider + * @param array $dateData + * @param array $productOptionData + * @param array $requestData + * @param string $expectedOptionValueForRequest + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoConfigFixture current_store general/locale/timezone Asia/Singapore + */ + public function testPrepareForCart( + array $dateData, + array $productOptionData, + array $requestData, + string $expectedOptionValueForRequest + ) { + $this->model->setData($dateData); + /** @var Option|null $productOption */ + $productOption = $productOptionData + ? $this->objectManager->create( + Option::class, + ['data' => $productOptionData] + ) + : null; + $this->model->setOption($productOption); + $request = new DataObject(); + $request->setData($requestData); + $this->model->setRequest($request); + $actualOptionValueForRequest = $this->model->prepareForCart(); + $this->assertSame($expectedOptionValueForRequest, $actualOptionValueForRequest); + } + + /** + * Data provider for testPrepareForCart + * + * @return array + */ + public function testPrepareForCartDataProvider() + { + return [ + [ + // $dateData + [ + 'is_valid' => true, + 'user_value' => [ + 'date' => '09/30/2019', + 'year' => 0, + 'month' => 0, + 'day' => 0, + 'hour' => 0, + 'minute' => 0, + 'day_part' => '', + 'date_internal' => '' + ] + ], + // $productOptionData + ['id' => '11', 'value' => '{"qty":12}', 'type' => 'date'], + // $requestData + [ + 'options' => [ + [ + 'date' => '09/30/2019' + ] + ] + ], + // $expectedOptionValueForRequest + '2019-09-30 00:00:00' + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/OptionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/OptionTest.php deleted file mode 100644 index bfb49686447c0..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/OptionTest.php +++ /dev/null @@ -1,149 +0,0 @@ -productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); - $this->customOptionFactory = Bootstrap::getObjectManager()->create(ProductCustomOptionInterfaceFactory::class); - $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); - } - - /** - * Tests removing ineligible characters from file_extension. - * - * @param string $rawExtensions - * @param string $expectedExtensions - * @dataProvider fileExtensionsDataProvider - * @magentoDataFixture Magento/Catalog/_files/product_without_options.php - */ - public function testFileExtensions(string $rawExtensions, string $expectedExtensions) - { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->get('simple'); - /** @var \Magento\Catalog\Model\Product\Option $fileOption */ - $fileOption = $this->createFileOption($rawExtensions); - $product->addOption($fileOption); - $product->save(); - $product = $this->productRepository->get('simple'); - $fileOption = $product->getOptions()[0]; - $actualExtensions = $fileOption->getFileExtension(); - $this->assertEquals($expectedExtensions, $actualExtensions); - } - - /** - * Data provider for testFileExtensions. - * - * @return array - */ - public function fileExtensionsDataProvider() - { - return [ - ['JPG, PNG, GIF', 'jpg, png, gif'], - ['jpg, jpg, jpg', 'jpg'], - ['jpg, png, gif', 'jpg, png, gif'], - ['jpg png gif', 'jpg, png, gif'], - ['!jpg@png#gif%', 'jpg, png, gif'], - ['jpg, png, 123', 'jpg, png, 123'], - ['', ''], - ]; - } - - /** - * Create file type option for product. - * - * @param string $rawExtensions - * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface|void - */ - private function createFileOption(string $rawExtensions) - { - $data = [ - 'title' => 'file option', - 'type' => 'file', - 'is_require' => true, - 'sort_order' => 3, - 'price' => 30.0, - 'price_type' => 'percent', - 'sku' => 'sku3', - 'file_extension' => $rawExtensions, - 'image_size_x' => 10, - 'image_size_y' => 20, - ]; - - return $this->customOptionFactory->create(['data' => $data]); - } - - /** - * Test to save option price by store - * - * @magentoDataFixture Magento/Catalog/_files/product_with_options.php - * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php - * @magentoConfigFixture default_store catalog/price/scope 1 - * @magentoConfigFixture secondstore_store catalog/price/scope 1 - */ - public function testSaveOptionPriceByStore() - { - $secondWebsitePrice = 22.0; - $defaultStoreId = $this->storeManager->getStore()->getId(); - $secondStoreId = $this->storeManager->getStore('secondstore')->getId(); - - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->get('simple'); - $option = $product->getOptions()[0]; - $defaultPrice = $option->getPrice(); - - $option->setPrice($secondWebsitePrice); - $product->setStoreId($secondStoreId); - // set Current store='secondstore' to correctly save product options for 'secondstore' - $this->storeManager->setCurrentStore($secondStoreId); - $this->productRepository->save($product); - $this->storeManager->setCurrentStore($defaultStoreId); - - $product = $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID, true); - $option = $product->getOptions()[0]; - $this->assertEquals($defaultPrice, $option->getPrice(), 'Price value by default store is wrong'); - - $product = $this->productRepository->get('simple', false, $secondStoreId, true); - $option = $product->getOptions()[0]; - $this->assertEquals($secondWebsitePrice, $option->getPrice(), 'Price value by store_id=1 is wrong'); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateCustomOptionsTest.php new file mode 100644 index 0000000000000..c07303f03e4f1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateCustomOptionsTest.php @@ -0,0 +1,465 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->optionRepository = $this->objectManager->get(ProductCustomOptionRepositoryInterface::class); + $this->customOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); + $this->customOptionValueFactory = $this->objectManager + ->get(ProductCustomOptionValuesInterfaceFactory::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->currentStoreId = $this->storeManager->getStore()->getId(); + $adminStoreId = $this->storeManager->getStore('admin')->getId(); + $this->storeManager->setCurrentStore($adminStoreId); + + parent::setUp(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->storeManager->setCurrentStore($this->currentStoreId); + + parent::tearDown(); + } + + /** + * Test update product custom options with type "area". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Area::getDataForUpdateOptions + * + * @param array $optionData + * @param array $updateData + * @return void + */ + public function testUpdateAreaCustomOption(array $optionData, array $updateData): void + { + $this->updateAndAssertNotSelectCustomOptions($optionData, $updateData); + } + + /** + * Test update product custom options with type "file". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\File::getDataForUpdateOptions + * + * @param array $optionData + * @param array $updateData + * @return void + */ + public function testUpdateFileCustomOption(array $optionData, array $updateData): void + { + $this->updateAndAssertNotSelectCustomOptions($optionData, $updateData); + } + + /** + * Test update product custom options with type "Date". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Date::getDataForUpdateOptions + * + * @param array $optionData + * @param array $updateData + * @return void + */ + public function testUpdateDateCustomOption(array $optionData, array $updateData): void + { + $this->updateAndAssertNotSelectCustomOptions($optionData, $updateData); + } + + /** + * Test update product custom options with type "Date & Time". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\DateTime::getDataForUpdateOptions + * + * @param array $optionData + * @param array $updateData + * @return void + */ + public function testUpdateDateTimeCustomOption(array $optionData, array $updateData): void + { + $this->updateAndAssertNotSelectCustomOptions($optionData, $updateData); + } + + /** + * Test update product custom options with type "Time". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Time::getDataForUpdateOptions + * + * @param array $optionData + * @param array $updateData + * @return void + */ + public function testUpdateTimeCustomOption(array $optionData, array $updateData): void + { + $this->updateAndAssertNotSelectCustomOptions($optionData, $updateData); + } + + /** + * Test update product custom options with type "Drop-down". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\DropDown::getDataForUpdateOptions + * + * @param array $optionData + * @param array $optionValueData + * @param array $updateOptionData + * @param array $updateOptionValueData + * @return void + */ + public function testUpdateDropDownCustomOption( + array $optionData, + array $optionValueData, + array $updateOptionData, + array $updateOptionValueData + ): void { + $this->updateAndAssertSelectCustomOptions( + $optionData, + $optionValueData, + $updateOptionData, + $updateOptionValueData + ); + } + + /** + * Test update product custom options with type "Radio Buttons". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\RadioButtons::getDataForUpdateOptions + * + * @param array $optionData + * @param array $optionValueData + * @param array $updateOptionData + * @param array $updateOptionValueData + * @return void + */ + public function testUpdateRadioButtonsCustomOption( + array $optionData, + array $optionValueData, + array $updateOptionData, + array $updateOptionValueData + ): void { + $this->updateAndAssertSelectCustomOptions( + $optionData, + $optionValueData, + $updateOptionData, + $updateOptionValueData + ); + } + + /** + * Test update product custom options with type "Checkbox". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\Checkbox::getDataForUpdateOptions + * + * @param array $optionData + * @param array $optionValueData + * @param array $updateOptionData + * @param array $updateOptionValueData + * @return void + */ + public function testUpdateCheckboxCustomOption( + array $optionData, + array $optionValueData, + array $updateOptionData, + array $updateOptionValueData + ): void { + $this->updateAndAssertSelectCustomOptions( + $optionData, + $optionValueData, + $updateOptionData, + $updateOptionValueData + ); + } + + /** + * Test update product custom options with type "Multiple Select". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider \Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\MultipleSelect::getDataForUpdateOptions + * + * @param array $optionData + * @param array $optionValueData + * @param array $updateOptionData + * @param array $updateOptionValueData + * @return void + */ + public function testUpdateMultipleSelectCustomOption( + array $optionData, + array $optionValueData, + array $updateOptionData, + array $updateOptionValueData + ): void { + $this->updateAndAssertSelectCustomOptions( + $optionData, + $optionValueData, + $updateOptionData, + $updateOptionValueData + ); + } + + /** + * Update product custom options which are not from "select" group and assert updated data. + * + * @param array $optionData + * @param array $updateData + * @return void + */ + private function updateAndAssertNotSelectCustomOptions(array $optionData, array $updateData): void + { + $productSku = 'simple'; + $createdOption = $this->createCustomOption($optionData, $productSku); + $updatedOption = $this->updateOptionWithValues($updateData, $productSku); + + foreach ($updateData as $methodKey => $newValue) { + $this->assertEquals($newValue, $updatedOption->getDataUsingMethod($methodKey)); + $this->assertNotEquals( + $createdOption->getDataUsingMethod($methodKey), + $updatedOption->getDataUsingMethod($methodKey) + ); + } + + $this->assertEquals($createdOption->getOptionId(), $updatedOption->getOptionId()); + } + + /** + * Update product custom options which from "select" group and assert updated data. + * + * @param array $optionData + * @param array $optionValueData + * @param array $updateOptionData + * @param array $updateOptionValueData + * @return void + */ + private function updateAndAssertSelectCustomOptions( + array $optionData, + array $optionValueData, + array $updateOptionData, + array $updateOptionValueData + ): void { + $productSku = 'simple'; + $createdOption = $this->createCustomOptionWithValue($optionData, $optionValueData, $productSku); + $createdOptionValue = $this->getOptionValue($createdOption); + $updatedOption = $this->updateOptionAndValueWithValues($updateOptionData, $updateOptionValueData, $productSku); + $updatedOptionValue = $this->getOptionValue($updatedOption); + + foreach ($updateOptionData as $methodKey => $newValue) { + $this->assertEquals($newValue, $updatedOption->getDataUsingMethod($methodKey)); + $this->assertNotEquals( + $createdOption->getDataUsingMethod($methodKey), + $updatedOption->getDataUsingMethod($methodKey) + ); + } + + foreach ($updateOptionValueData as $methodKey => $newValue) { + $methodName = str_replace('_', '', ucwords($methodKey, '_')); + $this->assertEquals($newValue, $updatedOptionValue->{'get' . $methodName}()); + $this->assertNotEquals( + $createdOptionValue->getDataUsingMethod($methodKey), + $updatedOptionValue->getDataUsingMethod($methodKey) + ); + } + + $this->assertEquals($createdOption->getOptionId(), $updatedOption->getOptionId()); + } + + /** + * Create custom option and save product with created option. + * + * @param array $optionData + * @param string $productSku + * @return ProductCustomOptionInterface|Option + */ + private function createCustomOption(array $optionData, string $productSku): ProductCustomOptionInterface + { + $product = $this->productRepository->get($productSku); + $createdOption = $this->customOptionFactory->create(['data' => $optionData]); + $createdOption->setProductSku($product->getSku()); + $product->setOptions([$createdOption]); + $this->productRepository->save($product); + $productCustomOptions = $this->optionRepository->getProductOptions($product); + $option = reset($productCustomOptions); + + return $option; + } + + /** + * Create custom option from select group and save product with created option. + * + * @param array $optionData + * @param array $optionValueData + * @param string $productSku + * @return ProductCustomOptionInterface|Option + */ + private function createCustomOptionWithValue( + array $optionData, + array $optionValueData, + string $productSku + ): ProductCustomOptionInterface { + $optionValue = $this->customOptionValueFactory->create(['data' => $optionValueData]); + $optionData['values'] = [$optionValue]; + + return $this->createCustomOption($optionData, $productSku); + } + + /** + * Update product option with values. + * + * @param array $updateData + * @param string $productSku + * @return ProductCustomOptionInterface|Option + */ + private function updateOptionWithValues(array $updateData, string $productSku): ProductCustomOptionInterface + { + $product = $this->productRepository->get($productSku); + $currentOption = $this->getProductOptionByProductSku($product->getSku()); + $currentOption->setProductSku($product->getSku()); + foreach ($updateData as $methodKey => $newValue) { + $currentOption->setDataUsingMethod($methodKey, $newValue); + } + $product->setOptions([$currentOption]); + $this->productRepository->save($product); + + return $this->getProductOptionByProductSku($product->getSku()); + } + + /** + * Update product option with values. + * + * @param array $optionUpdateData + * @param array $optionValueUpdateData + * @param string $productSku + * @return ProductCustomOptionInterface|Option + */ + private function updateOptionAndValueWithValues( + array $optionUpdateData, + array $optionValueUpdateData, + string $productSku + ): ProductCustomOptionInterface { + $product = $this->productRepository->get($productSku); + $currentOption = $this->getProductOptionByProductSku($product->getSku()); + $currentOption->setProductSku($product->getSku()); + $optionValue = $this->getOptionValue($currentOption); + foreach ($optionUpdateData as $methodKey => $newValue) { + $currentOption->setDataUsingMethod($methodKey, $newValue); + } + foreach ($optionValueUpdateData as $methodKey => $newValue) { + $optionValue->setDataUsingMethod($methodKey, $newValue); + } + $currentOption->setValues([$optionValue]); + $product->setOptions([$currentOption]); + $this->productRepository->save($product); + + return $this->getProductOptionByProductSku($product->getSku()); + } + + /** + * Get product option by product sku. + * + * @param string $productSku + * @return ProductCustomOptionInterface|Option + */ + private function getProductOptionByProductSku(string $productSku): ProductCustomOptionInterface + { + $product = $this->productRepository->get($productSku); + $currentOptions = $this->optionRepository->getProductOptions($product); + + return reset($currentOptions); + } + + /** + * Return custom option value. + * + * @param ProductCustomOptionInterface $customOption + * @return ProductCustomOptionValuesInterface|Value + */ + private function getOptionValue(ProductCustomOptionInterface $customOption): ProductCustomOptionValuesInterface + { + $optionValues = $customOption->getValues(); + + return reset($optionValues); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php new file mode 100644 index 0000000000000..646e661419292 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php @@ -0,0 +1,106 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productWebsiteLink = $this->objectManager->get(Link::class); + $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoDataFixture Magento/Store/_files/website.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testAssignProductToWebsite(): void + { + $defaultWebsiteId = $this->websiteRepository->get('base')->getId(); + $secondWebsiteId = $this->websiteRepository->get('test')->getId(); + $product = $this->updateProductWebsites('simple2', [$defaultWebsiteId, $secondWebsiteId]); + $this->assertEquals( + [$defaultWebsiteId, $secondWebsiteId], + $this->productWebsiteLink->getWebsiteIdsByProductId($product->getId()) + ); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/product_two_websites.php + * @return void + */ + public function testUnassignProductFromWebsite(): void + { + $secondWebsiteId = $this->websiteRepository->get('test')->getId(); + $product = $this->updateProductWebsites('simple-on-two-websites', [$secondWebsiteId]); + $this->assertEquals([$secondWebsiteId], $this->productWebsiteLink->getWebsiteIdsByProductId($product->getId())); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testAssignNonExistingWebsite(): void + { + $messageFormat = 'The website with id %s that was requested wasn\'t found. Verify the website and try again.'; + $nonExistingWebsiteId = 921564; + $this->expectException(NoSuchEntityException::class); + $this->expectExceptionMessage((string)__(sprintf($messageFormat, $nonExistingWebsiteId))); + $this->updateProductWebsites('simple2', [$nonExistingWebsiteId]); + } + + /** + * Update product websites attribute + * + * @param string $productSku + * @param array $websiteIds + * @return ProductInterface + */ + private function updateProductWebsites(string $productSku, array $websiteIds): ProductInterface + { + $product = $this->productRepository->get($productSku); + $product->setWebsiteIds($websiteIds); + + return $this->productRepository->save($product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php index 4cf059d4bf692..ff2979150954e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php @@ -5,11 +5,15 @@ */ namespace Magento\Catalog\Model\Product; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; + /** * Test class for \Magento\Catalog\Model\Product\Url. * * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UrlTest extends \PHPUnit\Framework\TestCase { @@ -49,6 +53,7 @@ public function testGetUrlInStore() * @magentoConfigFixture fixturestore_store web/unsecure/base_url http://sample-second.com/ * @magentoConfigFixture fixturestore_store web/unsecure/base_link_url http://sample-second.com/ * @magentoDataFixture Magento/Catalog/_files/product_simple_multistore.php + * @magentoDbIsolation disabled * @dataProvider getUrlsWithSecondStoreProvider * @magentoAppArea adminhtml */ @@ -82,6 +87,9 @@ public function getUrlsWithSecondStoreProvider() ]; } + /** + * @magentoDbIsolation disabled + */ public function testGetProductUrl() { $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -116,6 +124,7 @@ public function testGetUrlPath() } /** + * @magentoDbIsolation disabled * @magentoAppArea frontend */ public function testGetUrl() @@ -132,4 +141,49 @@ public function testGetUrl() $product->setId(100); $this->assertContains('catalog/product/view/id/100/', $this->_model->getUrl($product)); } + + /** + * Check that rearranging product url rewrites do not influence on whether to use category in product links + * + * @magentoConfigFixture current_store catalog/seo/product_use_categories 0 + * @magentoDbIsolation disabled + */ + public function testGetProductUrlWithRearrangedUrlRewrites() + { + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class + ); + $categoryRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\CategoryRepository::class + ); + $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Registry::class + ); + $urlFinder = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\UrlRewrite\Model\UrlFinderInterface::class + ); + $urlPersist = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\UrlRewrite\Model\UrlPersistInterface::class + ); + + $product = $productRepository->get('simple'); + $category = $categoryRepository->get($product->getCategoryIds()[0]); + $registry->register('current_category', $category); + $this->assertNotContains($category->getUrlPath(), $this->_model->getProductUrl($product)); + + $rewrites = $urlFinder->findAllByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE + ] + ); + $this->assertGreaterThan(1, count($rewrites)); + foreach ($rewrites as $rewrite) { + if ($rewrite->getRequestPath() === 'simple-product.html') { + $rewrite->setUrlRewriteId($rewrite->getUrlRewriteId() + 1000); + } + } + $urlPersist->replace($rewrites); + $this->assertNotContains($category->getUrlPath(), $this->_model->getProductUrl($product)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php new file mode 100644 index 0000000000000..9481702183327 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php @@ -0,0 +1,75 @@ +objectManager = Bootstrap::getObjectManager(); + $this->hydratorPool = $this->objectManager->create(HydratorPool::class); + } + + /** + * Test that Hydrator correctly populates entity with data + */ + public function testProductHydrator() + { + $addAttributes = [ + 'sku' => 'product_updated', + 'name' => 'Product (Updated)', + 'type_id' => 'simple', + 'status' => 1, + ]; + + /** @var Product $product */ + $product = $this->objectManager->create(Product::class); + $product->setId(42) + ->setSku('product') + ->setName('Product') + ->setPrice(10) + ->setQty(123); + $product->lockAttribute('sku'); + $product->lockAttribute('type_id'); + $product->lockAttribute('price'); + + /** @var HydratorInterface $hydrator */ + $hydrator = $this->hydratorPool->getHydrator(ProductInterface::class); + $hydrator->hydrate($product, $addAttributes); + + $expected = [ + 'entity_id' => 42, + 'sku' => 'product_updated', + 'name' => 'Product (Updated)', + 'type_id' => 'simple', + 'status' => 1, + 'price' => 10, + 'qty' => 123, + ]; + $this->assertEquals($expected, $product->getData()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductLink/ProductLinkQueryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductLink/ProductLinkQueryTest.php new file mode 100644 index 0000000000000..8509174e127e7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductLink/ProductLinkQueryTest.php @@ -0,0 +1,145 @@ +query = $objectManager->get(ProductLinkQuery::class); + $this->productRepo = $objectManager->get(ProductRepository::class); + $this->criteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + } + + /** + * Generate search criteria. + * + * @param \Magento\Catalog\Model\Product[] $products + * @return ListCriteriaInterface[] + */ + private function generateCriteriaList(array $products): array + { + $typesList = ['related', 'crosssell', 'upsell']; + /** @var ListCriteriaInterface[] $criteriaList */ + $criteriaList = []; + foreach ($products as $product) { + $sku = $product->getSku(); + $typesFilter = [$typesList[rand(0, 2)], $typesList[rand(0, 2)]]; + //Not always providing product entity or the default criteria implementation for testing purposes. + //Getting 1 list with types filter and one without. + $criteriaList[] = new ListCriteria($sku, $typesFilter, $product); + $criteria = new class implements ListCriteriaInterface + { + /** + * @var string + */ + public $sku; + + /** + * @inheritDoc + */ + public function getBelongsToProductSku(): string + { + return $this->sku; + } + + /** + * @inheritDoc + */ + public function getLinkTypes(): ?array + { + return null; + } + }; + $criteria->sku = $sku; + $criteriaList[] = $criteria; + } + + return $criteriaList; + } + + /** + * Test getting links for a list of products. + * + * @magentoDataFixture Magento/Catalog/_files/multiple_related_products.php + * @return void + * @throws \Throwable + */ + public function testSearch(): void + { + //Finding root products + $list = $this->productRepo->getList( + $this->criteriaBuilder->addFilter('sku', 'simple-related-%', 'like')->create() + ); + //Creating criteria + $criteriaList = $this->generateCriteriaList($list->getItems()); + $this->assertNotEmpty($criteriaList); + //Searching + $result = $this->query->search($criteriaList); + //Checking results + $this->assertCount(count($criteriaList), $result); + foreach ($criteriaList as $index => $criteria) { + //No errors, links must be found + $this->assertNull($result[$index]->getError()); + if (!$criteria->getLinkTypes()) { + //If there were no types filter the list cannot be empty + $this->assertNotEmpty($result[$index]->getResult()); + } + foreach ($result[$index]->getResult() as $link) { + //Links must belong to requested products. + $this->assertEquals($criteria->getBelongsToProductSku(), $link->getSku()); + if ($criteria->getLinkTypes()) { + //If link filter was set no other link types must be returned + $this->assertContains($link->getLinkType(), $criteria->getLinkTypes()); + } + //Type must be accurate + $this->assertEquals(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE, $link->getLinkedProductType()); + //Determining whether the product is supposed to be linked by SKU + preg_match('/^simple\-related\-(\d+)$/i', $criteria->getBelongsToProductSku(), $productIndex); + $this->assertNotEmpty($productIndex); + $this->assertFalse(empty($productIndex[1])); + $productIndex = (int)$productIndex[1]; + $this->assertRegExp('/^related\-product\-' .$productIndex .'\-\d+$/i', $link->getLinkedProductSku()); + //Position must be set + $this->assertGreaterThan(0, $link->getPosition()); + } + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index fa2a0e5cb34b7..fb07d08faca58 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -11,6 +11,8 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Bootstrap as TestBootstrap; use Magento\Framework\Acl\Builder; @@ -46,15 +48,10 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase */ private $productResource; - /* - * @var Auth - */ - private $auth; - /** - * @var Builder + * @var ProductLayoutUpdateManager */ - private $aclBuilder; + private $layoutManager; /** * Sets up common objects @@ -63,21 +60,19 @@ protected function setUp() { $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); - $this->auth = Bootstrap::getObjectManager()->get(Auth::class); - $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); $this->productFactory = Bootstrap::getObjectManager()->get(ProductFactory::class); $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); + $this->layoutManager = Bootstrap::getObjectManager()->get(ProductLayoutUpdateManager::class); } /** - * @inheritDoc + * Create new subject instance. + * + * @return ProductRepositoryInterface */ - protected function tearDown() + private function createRepo(): ProductRepositoryInterface { - parent::tearDown(); - - $this->auth->logout(); - $this->aclBuilder->resetRuntimeAcl(); + return Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); } /** @@ -206,30 +201,36 @@ public function testUpdateProductSku() } /** - * Test authorization when saving product's design settings. + * Test that custom layout file attribute is saved. * + * @return void + * @throws \Throwable * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled */ - public function testSaveDesign() + public function testCustomLayout(): void { - $product = $this->productRepository->get('simple'); - $this->auth->login(TestBootstrap::ADMIN_NAME, TestBootstrap::ADMIN_PASSWORD); - - //Admin doesn't have access to product's design. - $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_product_design'); - - $product->setCustomAttribute('custom_design', 2); - $product = $this->productRepository->save($product); - $this->assertEmpty($product->getCustomAttribute('custom_design')); - - //Admin has access to products' design. - $this->aclBuilder->getAcl() - ->allow(null, ['Magento_Catalog::products','Magento_Catalog::edit_product_design']); - - $product->setCustomAttribute('custom_design', 2); - $product = $this->productRepository->save($product); - $this->assertNotEmpty($product->getCustomAttribute('custom_design')); - $this->assertEquals(2, $product->getCustomAttribute('custom_design')->getValue()); + //New valid value + $repo = $this->createRepo(); + $product = $repo->get('simple'); + $newFile = 'test'; + $this->layoutManager->setFakeFiles((int)$product->getId(), [$newFile]); + $product->setCustomAttribute('custom_layout_update_file', $newFile); + $repo->save($product); + $repo = $this->createRepo(); + $product = $repo->get('simple'); + $this->assertEquals($newFile, $product->getCustomAttribute('custom_layout_update_file')->getValue()); + + //Setting non-existent value + $newFile = 'does not exist'; + $product->setCustomAttribute('custom_layout_update_file', $newFile); + $caughtException = false; + try { + $repo->save($product); + } catch (LocalizedException $exception) { + $caughtException = true; + } + $this->assertTrue($caughtException); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index c34120404a950..d7da47ef78724 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -8,7 +8,12 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Model\Product; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; /** * Tests product model: @@ -26,31 +31,32 @@ class ProductTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; /** - * @var \Magento\Catalog\Model\Product + * @var Product */ protected $_model; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * @inheritdoc */ protected function setUp() { - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->_model = $this->objectManager->create(Product::class); } /** - * @throws \Magento\Framework\Exception\FileSystemException - * @return void + * @inheritdoc */ public static function tearDownAfterClass() { @@ -74,6 +80,8 @@ public static function tearDownAfterClass() } /** + * Test can affect options + * * @return void */ public function testCanAffectOptions() @@ -84,6 +92,8 @@ public function testCanAffectOptions() } /** + * Test CRUD + * * @magentoDbIsolation enabled * @magentoAppIsolation enabled * @magentoAppArea adminhtml @@ -116,6 +126,8 @@ public function testCRUD() } /** + * Test clean cache + * * @return void */ public function testCleanCache() @@ -139,6 +151,8 @@ public function testCleanCache() } /** + * Test add image to media gallery + * * @return void */ public function testAddImageToMediaGallery() @@ -183,6 +197,8 @@ protected function _copyFileToBaseTmpMediaPath($sourceFile) } /** + * Test duplicate method + * * @magentoAppIsolation enabled * @magentoAppArea adminhtml */ @@ -213,6 +229,8 @@ public function testDuplicate() } /** + * Test duplicate sku generation + * * @magentoAppArea adminhtml */ public function testDuplicateSkuGeneration() @@ -244,6 +262,8 @@ protected function _undo($duplicate) } /** + * Test visibility api + * * @covers \Magento\Catalog\Model\Product::getVisibleInCatalogStatuses * @covers \Magento\Catalog\Model\Product::getVisibleStatuses * @covers \Magento\Catalog\Model\Product::isVisibleInCatalog @@ -286,6 +306,8 @@ public function testVisibilityApi() } /** + * Test isDuplicable and setIsDuplicable methods + * * @covers \Magento\Catalog\Model\Product::isDuplicable * @covers \Magento\Catalog\Model\Product::setIsDuplicable */ @@ -297,6 +319,8 @@ public function testIsDuplicable() } /** + * Test isSalable, isSaleable, isAvailable and isInStock methods + * * @covers \Magento\Catalog\Model\Product::isSalable * @covers \Magento\Catalog\Model\Product::isSaleable * @covers \Magento\Catalog\Model\Product::isAvailable @@ -314,6 +338,8 @@ public function testIsSalable() } /** + * Test isSalable method when Status is disabled + * * @covers \Magento\Catalog\Model\Product::isSalable * @covers \Magento\Catalog\Model\Product::isSaleable * @covers \Magento\Catalog\Model\Product::isAvailable @@ -331,6 +357,8 @@ public function testIsNotSalableWhenStatusDisabled() } /** + * Test isVirtual and getIsVirtual methods + * * @covers \Magento\Catalog\Model\Product::isVirtual * @covers \Magento\Catalog\Model\Product::getIsVirtual */ @@ -349,6 +377,8 @@ public function testIsVirtual() } /** + * Test toArray method + * * @return void */ public function testToArray() @@ -359,6 +389,8 @@ public function testToArray() } /** + * Test fromArray method + * * @return void */ public function testFromArray() @@ -368,6 +400,8 @@ public function testFromArray() } /** + * Test set original data backend + * * @magentoAppArea adminhtml */ public function testSetOrigDataBackend() @@ -378,6 +412,8 @@ public function testSetOrigDataBackend() } /** + * Test reset method + * * @magentoAppArea frontend */ public function testReset() @@ -418,6 +454,8 @@ protected function _assertEmpty($model) } /** + * Test is products has sku + * * @magentoDataFixture Magento/Catalog/_files/multiple_products.php */ public function testIsProductsHasSku() @@ -433,6 +471,8 @@ public function testIsProductsHasSku() } /** + * Test process by request + * * @return void */ public function testProcessBuyRequest() @@ -444,6 +484,8 @@ public function testProcessBuyRequest() } /** + * Test validate method + * * @return void */ public function testValidate() @@ -480,6 +522,8 @@ public function testValidate() } /** + * Test validate unique input attribute value + * * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/_files/products_with_unique_input_attribute.php */ @@ -523,6 +567,8 @@ public function testValidateUniqueInputAttributeValue() } /** + * Test validate unique input attribute value on the same product + * * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/_files/products_with_unique_input_attribute.php */ @@ -618,8 +664,53 @@ public function testSaveWithBackordersEnabled(int $qty, int $stockStatus, bool $ $this->assertEquals($expectedStockStatus, $stockItem->getIsInStock()); } + /** + * Checking enable/disable product when Catalog Flat Product is enabled + * + * @magentoAppArea frontend + * @magentoDbIsolation disabled + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException + */ + public function testProductStatusWhenCatalogFlatProductIsEnabled() + { + // check if product flat table is enabled + $productFlatState = $this->objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\State::class); + $this->assertTrue($productFlatState->isFlatEnabled()); + // run reindex to create product flat table + $productFlatProcessor = $this->objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\Processor::class); + $productFlatProcessor->reindexAll(); + // get created simple product + $product = $this->productRepository->get('simple'); + // get db connection and the product flat table name + $resource = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = $resource->getConnection(); + $productFlatTableName = $productFlatState->getFlatIndexerHelper()->getFlatTableName(1); + // generate sql query to find created simple product in the flat table + $sql = $connection->select()->from($productFlatTableName)->where('sku =?', $product->getSku()); + // check if the product exists in the product flat table + $products = $connection->fetchAll($sql); + $this->assertEquals(Status::STATUS_ENABLED, $product->getStatus()); + $this->assertNotEmpty($products); + // disable product + $product->setStatus(Status::STATUS_DISABLED); + $product = $this->productRepository->save($product); + // check if the product exists in the product flat table + $products = $connection->fetchAll($sql); + $this->assertEquals(Status::STATUS_DISABLED, $product->getStatus()); + $this->assertEmpty($products); + } + /** * DataProvider for the testSaveWithBackordersEnabled() + * * @return array */ public function productWithBackordersDataProvider(): array diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php new file mode 100644 index 0000000000000..8ecf3da8e1aae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php @@ -0,0 +1,144 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepository::class); + $this->model = $this->objectManager->get(Attribute::class); + } + + /** + * Test to Clear selected option in entities after remove + */ + public function testClearSelectedOptionInEntities() + { + $dropdownAttribute = $this->loadAttribute('dropdown_attribute'); + $dropdownOption = array_keys($dropdownAttribute->getOptions())[1]; + + $multiplyAttribute = $this->loadAttribute('multiselect_attribute'); + $multiplyOptions = array_keys($multiplyAttribute->getOptions()); + $multiplySelectedOptions = implode(',', $multiplyOptions); + $multiplyOptionToRemove = $multiplyOptions[1]; + unset($multiplyOptions[1]); + $multiplyOptionsExpected = implode(',', $multiplyOptions); + + $product = $this->loadProduct('simple'); + $product->setData('dropdown_attribute', $dropdownOption); + $product->setData('multiselect_attribute', $multiplySelectedOptions); + $this->productRepository->save($product); + + $product = $this->loadProduct('simple'); + $this->assertEquals( + $dropdownOption, + $product->getData('dropdown_attribute'), + 'The dropdown attribute is not selected' + ); + $this->assertEquals( + $multiplySelectedOptions, + $product->getData('multiselect_attribute'), + 'The multiselect attribute is not selected' + ); + + $this->removeAttributeOption($dropdownAttribute, $dropdownOption); + $this->removeAttributeOption($multiplyAttribute, $multiplyOptionToRemove); + + $product = $this->loadProduct('simple'); + $this->assertEmpty($product->getData('dropdown_attribute')); + $this->assertEquals($multiplyOptionsExpected, $product->getData('multiselect_attribute')); + } + + /** + * Remove option from attribute + * + * @param Attribute $attribute + * @param int $optionId + */ + private function removeAttributeOption(Attribute $attribute, int $optionId): void + { + $removalMarker = [ + 'option' => [ + 'value' => [$optionId => []], + 'delete' => [$optionId => '1'], + ], + ]; + $attribute->addData($removalMarker); + $attribute->save($attribute); + } + + /** + * Load product by sku + * + * @param string $sku + * @return Product + */ + private function loadProduct(string $sku): Product + { + return $this->productRepository->get($sku, true, null, true); + } + + /** + * Load attrubute by code + * + * @param string $attributeCode + * @return Attribute + */ + private function loadAttribute(string $attributeCode): Attribute + { + /** @var Attribute $attribute */ + $attribute = $this->objectManager->create(Attribute::class); + $attribute->loadByCode(4, $attributeCode); + + return $attribute; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php index 349853c8a3935..498c3167ed734 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\ResourceModel\Eav; +/** + * Test for \Magento\Catalog\Model\ResourceModel\Eav\Attribute. + */ class AttributeTest extends \PHPUnit\Framework\TestCase { /** @@ -19,6 +22,11 @@ protected function setUp() ); } + /** + * Test Create -> Read -> Update -> Delete attribute operations. + * + * @return void + */ public function testCRUD() { $this->_model->setAttributeCode( @@ -31,7 +39,7 @@ public function testCRUD() )->getId() )->setFrontendLabel( 'test' - ); + )->setIsUserDefined(1); $crud = new \Magento\TestFramework\Entity($this->_model, ['frontend_label' => uniqid()]); $crud->testCrud(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php index 4cc6265a992fa..86dcf9d96d086 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php @@ -3,10 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + /** * Collection test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CollectionTest extends \PHPUnit\Framework\TestCase { @@ -31,15 +41,15 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->collection = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Product\Collection::class ); - $this->processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->processor = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\Indexer\Product\Price\Processor::class ); - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->productRepository = Bootstrap::getObjectManager()->create( \Magento\Catalog\Api\ProductRepositoryInterface::class ); } @@ -54,7 +64,7 @@ public function testAddPriceDataOnSchedule() $this->processor->getIndexer()->setScheduled(true); $this->assertTrue($this->processor->getIndexer()->isScheduled()); - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $productRepository = Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get('simple'); @@ -73,7 +83,7 @@ public function testAddPriceDataOnSchedule() //reindexing $this->processor->getIndexer()->reindexList([1]); - $this->collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->collection = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Product\Collection::class ); $this->collection->addPriceData(0, 1); @@ -89,6 +99,69 @@ public function testAddPriceDataOnSchedule() $this->processor->getIndexer()->setScheduled(false); } + /** + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ + public function testSetVisibility() + { + $appState = Bootstrap::getObjectManager() + ->create(State::class); + $appState->setAreaCode(Area::AREA_CRONTAB); + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); + $this->collection->setVisibility([Visibility::VISIBILITY_BOTH]); + $this->collection->load(); + /** @var \Magento\Catalog\Api\Data\ProductInterface[] $product */ + $items = $this->collection->getItems(); + $this->assertCount(2, $items); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ + public function testSetCategoryWithStoreFilter() + { + $appState = Bootstrap::getObjectManager() + ->create(State::class); + $appState->setAreaCode(Area::AREA_CRONTAB); + + $category = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Catalog\Model\Category::class + )->load(333); + $this->collection->addCategoryFilter($category)->addStoreFilter(1); + $this->collection->load(); + + $collectionStoreFilterAfter = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class + )->create(); + $collectionStoreFilterAfter->addStoreFilter(1)->addCategoryFilter($category); + $collectionStoreFilterAfter->load(); + $this->assertEquals($this->collection->getItems(), $collectionStoreFilterAfter->getItems()); + $this->assertCount(1, $collectionStoreFilterAfter->getItems()); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/categories.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ + public function testSetCategoryFilter() + { + $appState = Bootstrap::getObjectManager() + ->create(State::class); + $appState->setAreaCode(Area::AREA_CRONTAB); + + $category = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Catalog\Model\Category::class + )->load(3); + $this->collection->addCategoryFilter($category); + $this->collection->load(); + $this->assertEquals($this->collection->getSize(), 3); + } + /** * @magentoDataFixture Magento/Catalog/_files/products.php * @magentoAppIsolation enabled @@ -98,7 +171,7 @@ public function testAddPriceDataOnSave() { $this->processor->getIndexer()->setScheduled(false); $this->assertFalse($this->processor->getIndexer()->isScheduled()); - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $productRepository = Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get('simple'); @@ -184,7 +257,7 @@ public function testJoinTable() $productTable = $this->collection->getTable('catalog_product_entity'); $urlRewriteTable = $this->collection->getTable('url_rewrite'); - // phpcs:ignore + // phpcs:ignore Magento2.SQL.RawQuery $expected = 'SELECT `e`.*, `alias`.`request_path` FROM `' . $productTable . '` AS `e`' . ' LEFT JOIN `' . $urlRewriteTable . '` AS `alias` ON (alias.entity_id =e.entity_id)' . ' AND (alias.entity_type = \'product\')'; @@ -204,15 +277,57 @@ public function testAddAttributeToFilterAffectsGetSize(): void } /** - * Add tier price attribute filter to collection + * Add tier price attribute filter to collection with different condition types. * + * @param mixed $condition * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/few_simple_products.php * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/product_simple.php + * + * @dataProvider addAttributeTierPriceToFilterDataProvider */ - public function testAddAttributeTierPriceToFilter(): void + public function testAddAttributeTierPriceToFilter($condition): void { - $this->assertEquals(11, $this->collection->getSize()); - $this->collection->addAttributeToFilter('tier_price', ['gt' => 0]); + $this->collection->addAttributeToFilter('tier_price', $condition); $this->assertEquals(1, $this->collection->getSize()); } + + /** + * @return array + */ + public function addAttributeTierPriceToFilterDataProvider(): array + { + return [ + 'condition is array' => [['eq' => 8]], + 'condition is string' => ['8'], + 'condition is int' => [8], + 'condition is null' => [null] + ]; + } + + /** + * Add is_saleable attribute filter to collection with different condition types. + * + * @param mixed $condition + * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/product_simple.php + * + * @dataProvider addAttributeIsSaleableToFilterDataProvider + */ + public function testAddAttributeIsSaleableToFilter($condition): void + { + $this->collection->addAttributeToFilter('is_saleable', $condition); + $this->assertEquals(1, $this->collection->getSize()); + } + + /** + * @return array + */ + public function addAttributeIsSaleableToFilterDataProvider(): array + { + return [ + 'condition is array' => [['eq' => 1]], + 'condition is string' => ['1'], + 'condition is int' => [1], + 'condition is null' => [null] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php index 5cf6d00fe77ea..78ae21b1441bd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php @@ -7,12 +7,17 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Catalog\_files\MultiselectSourceMock; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; /** * Class SourceTest * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SourceTest extends \PHPUnit\Framework\TestCase { @@ -159,6 +164,42 @@ public function testReindexMultiselectAttribute() $this->assertCount(3, $result); } + /** + * Test for indexing product attribute without "all store view" value + * + * @magentoDataFixture Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view.php + * @magentoDbIsolation disabled + */ + public function testReindexSelectAttributeWithoutDefault() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var StoreInterface $store */ + $store = $objectManager->get(StoreManagerInterface::class) + ->getStore(); + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute **/ + $attribute = $objectManager->get(\Magento\Eav\Model\Config::class) + ->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'dropdown_without_default'); + /** @var AttributeOptionInterface $option */ + $option = $attribute->getOptions()[1]; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get('test_attribute_dropdown_without_default', false, 1); + $expected = [ + 'entity_id' => $product->getId(), + 'attribute_id' => $attribute->getId(), + 'store_id' => $store->getId(), + 'value' => $option->getValue(), + 'source_id' => $product->getId(), + ]; + $connection = $this->productResource->getConnection(); + $select = $connection->select()->from($this->productResource->getTable('catalog_product_index_eav')) + ->where('entity_id = ?', $product->getId()) + ->where('attribute_id = ?', $attribute->getId()); + + $result = $connection->fetchRow($select); + $this->assertEquals($expected, $result); + } + /** * @magentoDataFixture Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php * @magentoDbIsolation disabled diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index e218c508b7d3e..f560854fa75f6 100755 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -6,7 +6,12 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Product\Action; +use Magento\Eav\Api\Data\AttributeSetInterface; +use Magento\Eav\Model\AttributeSetRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Eav\Model\GetAttributeSetByName; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; use Magento\Framework\Exception\NoSuchEntityException; @@ -164,4 +169,33 @@ public function testUpdateStoreSpecificSpecialPrice() $product = $this->productRepository->get('simple', false, 0, true); $this->assertEquals(5.99, $product->getSpecialPrice()); } + + /** + * Checks that product has no attribute values for attributes not assigned to the product's attribute set. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/attribute_set_with_image_attribute.php + */ + public function testChangeAttributeSet() + { + $attributeCode = 'funny_image'; + /** @var GetAttributeSetByName $attributeSetModel */ + $attributeSetModel = $this->objectManager->get(GetAttributeSetByName::class); + $attributeSet = $attributeSetModel->execute('attribute_set_with_media_attribute'); + + $product = $this->productRepository->get('simple', true, 1, true); + $product->setAttributeSetId($attributeSet->getAttributeSetId()); + $this->productRepository->save($product); + $product->setData($attributeCode, 'test'); + $this->model->saveAttribute($product, $attributeCode); + + $product = $this->productRepository->get('simple', true, 1, true); + $this->assertEquals('test', $product->getData($attributeCode)); + + $product->setAttributeSetId($product->getDefaultAttributeSetId()); + $this->productRepository->save($product); + + $attribute = $this->model->getAttributeRawValue($product->getId(), $attributeCode, 1); + $this->assertEmpty($attribute); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php new file mode 100644 index 0000000000000..3375a4e8e4094 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php @@ -0,0 +1,306 @@ + 'input', + 'hidden' => 'input', + 'boolean' => 'checkbox', + 'media_image' => 'image', + 'price' => 'input', + 'weight' => 'input', + 'gallery' => 'image' + ]; + $this->objectManager = Bootstrap::getObjectManager(); + $this->locatorMock = $this->createMock(LocatorInterface::class); + $this->locatorMock->expects($this->any())->method('getStore')->willReturn( + $this->objectManager->get(StoreInterface::class) + ); + $this->metaPropertiesMapper = $this->objectManager->create(MetaProperties::class, ['mappings' => []]); + $this->compositeConfigProcessor = $this->objectManager->create( + CompositeConfigProcessor::class, + ['eavWysiwygDataProcessors' => []] + ); + $this->eavModifier = $this->objectManager->create( + Eav::class, + [ + 'locator' => $this->locatorMock, + 'formElementMapper' => $this->objectManager->create(FormElement::class, ['mappings' => $mappings]), + 'metaPropertiesMapper' => $this->metaPropertiesMapper, + 'wysiwygConfigProcessor' => $this->compositeConfigProcessor, + ] + ); + $this->attributeRepository = $this->objectManager->create(ProductAttributeRepositoryInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->defaultSetId = (int)$this->objectManager->create(Type::class) + ->loadByCode(ProductAttributeInterface::ENTITY_TYPE_CODE) + ->getDefaultAttributeSetId(); + } + + /** + * @param ProductInterface $product + * @param array $expectedMeta + * @return void + */ + protected function callModifyMetaAndAssert(ProductInterface $product, array $expectedMeta): void + { + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($product); + $actualMeta = $this->eavModifier->modifyMeta([]); + $this->prepareDataForComparison($actualMeta, $expectedMeta); + $this->assertEquals($expectedMeta, $actualMeta); + } + + /** + * @param ProductInterface $product + * @param array $expectedData + * @return void + */ + protected function callModifyDataAndAssert(ProductInterface $product, array $expectedData): void + { + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($product); + $actualData = $this->eavModifier->modifyData([]); + $this->prepareDataForComparison($actualData, $expectedData); + $this->assertEquals($expectedData, $actualData); + } + + /** + * Prepare data for comparison to avoid false positive failures. + * + * Make sure that $data contains all the data contained in $expectedData, + * ignore all fields not declared in $expectedData + * + * @param array &$data + * @param array $expectedData + * @return void + */ + protected function prepareDataForComparison(array &$data, array $expectedData): void + { + foreach ($data as $key => &$item) { + if (!isset($expectedData[$key])) { + unset($data[$key]); + continue; + } + if ($item instanceof Phrase) { + $item = (string)$item; + } elseif (is_array($item)) { + $this->prepareDataForComparison($item, $expectedData[$key]); + } elseif ($key === 'price_id' || $key === 'sortOrder') { + $data[$key] = '__placeholder__'; + } + } + } + + /** + * Updates attribute default value. + * + * @param string $attributeCode + * @param string $defaultValue + * @return void + */ + protected function setAttributeDefaultValue(string $attributeCode, string $defaultValue): void + { + $attribute = $this->attributeRepository->get($attributeCode); + $attribute->setDefaultValue($defaultValue); + $this->attributeRepository->save($attribute); + } + + /** + * Returns attribute options list. + * + * @param string $attributeCode + * @return array + */ + protected function getAttributeOptions(string $attributeCode): array + { + $attribute = $this->attributeRepository->get($attributeCode); + + return $attribute->usesSource() ? $attribute->getSource()->getAllOptions() : []; + } + + /** + * Returns attribute option value by id. + * + * @param string $attributeCode + * @param string $label + * @return int|null + */ + protected function getOptionValueByLabel(string $attributeCode, string $label): ?int + { + $result = null; + foreach ($this->getAttributeOptions($attributeCode) as $option) { + if ($option['label'] == $label) { + $result = (int)$option['value']; + } + } + + return $result; + } + + /** + * Returns product for testing. + * + * @return ProductInterface + */ + protected function getProduct(): ProductInterface + { + return $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID); + } + + /** + * Returns new product object. + * + * @return ProductInterface + */ + protected function getNewProduct(): ProductInterface + { + $product = $this->productFactory->create(); + $product->setAttributeSetId($this->defaultSetId); + + return $product; + } + + /** + * Updates product. + * + * @param ProductInterface $product + * @param array $attributeData + * @return void + */ + protected function saveProduct(ProductInterface $product, array $attributeData): void + { + $product->addData($attributeData); + $this->productRepository->save($product); + } + + /** + * Adds additional array nesting to expected meta. + * + * @param array $attributeMeta + * @param string $groupCode + * @param string $attributeCode + * @return array + */ + protected function addMetaNesting(array $attributeMeta, string $groupCode, string $attributeCode): array + { + return [ + $groupCode => [ + 'arguments' => ['data' => ['config' => ['dataScope' => 'data.product']]], + 'children' => [ + 'container_' . $attributeCode => [ + 'children' => [$attributeCode => ['arguments' => ['data' => ['config' => $attributeMeta]]]], + ], + ], + ], + ]; + } + + /** + * Adds additional array nesting to expected data. + * + * @param array $data + * @return array + */ + protected function addDataNesting(array $data): array + { + return [1 => ['product' => $data]]; + } + + /** + * Returns attribute codes from product meta data array. + * + * @param array $actualMeta + * @return array + */ + protected function getUsedAttributes(array $actualMeta): array + { + $usedAttributes = []; + foreach ($actualMeta as $group) { + foreach (array_keys($group['children']) as $field) { + $usedAttributes[] = str_replace('container_', '', $field); + } + } + + return $usedAttributes; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/AttributeSetGroupsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/AttributeSetGroupsTest.php new file mode 100644 index 0000000000000..4890d179c7d95 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/AttributeSetGroupsTest.php @@ -0,0 +1,36 @@ +locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getProduct()); + $meta = $this->eavModifier->modifyMeta([]); + $this->assertArrayNotHasKey( + 'test-attribute-group-name', + $meta, + 'Attribute set group without attributes appear on product page in admin panel' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/BooleanAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/BooleanAttributeTest.php new file mode 100644 index 0000000000000..16ad3d3ad4564 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/BooleanAttributeTest.php @@ -0,0 +1,86 @@ +callModifyMetaAndAssert( + $this->getProduct(), + $this->addMetaNesting($this->getAttributeMeta(), 'product-details', 'boolean_attribute') + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_boolean_attribute.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testModifyData(): void + { + $product = $this->getProduct(); + $attributeData = ['boolean_attribute' => 1]; + $this->saveProduct($product, $attributeData); + $expectedData = $this->addDataNesting($attributeData); + $this->callModifyDataAndAssert($product, $expectedData); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_boolean_attribute.php + * @return void + */ + public function testModifyMetaNewProduct(): void + { + $this->setAttributeDefaultValue('boolean_attribute', '0'); + $attributesMeta = array_merge($this->getAttributeMeta(), ['default' => '0']); + $expectedMeta = $this->addMetaNesting( + $attributesMeta, + 'product-details', + 'boolean_attribute' + ); + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } + + /** + * @return array + */ + private function getAttributeMeta(): array + { + return [ + 'dataType' => 'boolean', + 'formElement' => 'checkbox', + 'visible' => '1', + 'required' => '0', + 'label' => 'Boolean Attribute', + 'code' => 'boolean_attribute', + 'source' => 'product-details', + 'scopeLabel' => '[STORE VIEW]', + 'globalScope' => false, + 'sortOrder' => '__placeholder__', + 'componentType' => 'field', + 'prefer' => 'toggle', + 'valueMap' => [ + 'true' => '1', + 'false' => '0', + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DateAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DateAttributeTest.php new file mode 100644 index 0000000000000..feac956ddf549 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DateAttributeTest.php @@ -0,0 +1,81 @@ +callModifyMetaAndAssert( + $this->getProduct(), + $this->addMetaNesting($this->getAttributeMeta(), 'product-details', 'date_attribute') + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_date_attribute.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testModifyData(): void + { + $product = $this->getProduct(); + $attributeData = ['date_attribute' => '01/01/2010']; + $this->saveProduct($product, $attributeData); + $expectedData = $this->addDataNesting($attributeData); + $this->callModifyDataAndAssert($product, $expectedData); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_date_attribute.php + * @return void + */ + public function testModifyMetaNewProduct(): void + { + $this->setAttributeDefaultValue('date_attribute', '01/01/2000'); + $attributesMeta = array_merge($this->getAttributeMeta(), ['default' => '2000-01-01 00:00:00']); + $expectedMeta = $this->addMetaNesting( + $attributesMeta, + 'product-details', + 'date_attribute' + ); + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } + + /** + * @return array + */ + private function getAttributeMeta(): array + { + return [ + 'dataType' => 'date', + 'formElement' => 'date', + 'visible' => '1', + 'required' => '0', + 'label' => 'Date Attribute', + 'code' => 'date_attribute', + 'source' => 'product-details', + 'scopeLabel' => '[GLOBAL]', + 'globalScope' => true, + 'sortOrder' => '__placeholder__', + 'componentType' => 'field' + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DecimalAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DecimalAttributeTest.php new file mode 100644 index 0000000000000..901613498e53a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DecimalAttributeTest.php @@ -0,0 +1,67 @@ +callModifyMetaAndAssert( + $this->getProduct(), + $this->addMetaNesting($this->getAttributeMeta(), 'product-details', 'decimal_attribute') + ); + } + + /** + * @return void + */ + public function testModifyData(): void + { + $product = $this->getProduct(); + $attributeData = ['decimal_attribute' => '10.00']; + $this->saveProduct($product, $attributeData); + $expectedData = $this->addDataNesting($attributeData); + $this->callModifyDataAndAssert($product, $expectedData); + } + + /** + * @return array + */ + private function getAttributeMeta(): array + { + return [ + 'dataType' => 'price', + 'formElement' => 'input', + 'visible' => '1', + 'required' => '0', + 'label' => 'Decimal Attribute', + 'code' => 'decimal_attribute', + 'source' => 'product-details', + 'scopeLabel' => '[GLOBAL]', + 'globalScope' => true, + 'sortOrder' => '__placeholder__', + 'componentType' => 'field', + 'validation' => [ + 'validate-zero-or-greater' => true, + ], + 'addbefore' => '$' + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php new file mode 100644 index 0000000000000..fbf752cc9e239 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php @@ -0,0 +1,47 @@ +callModifyMetaAndAssert($this->getProduct(), $expectedMeta); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_admin_store.php + * @return void + */ + public function testModifyData(): void + { + $expectedData = include __DIR__ . '/../_files/eav_expected_data_output.php'; + $this->callModifyDataAndAssert($this->getProduct(), $expectedData); + } + + /** + * @return void + */ + public function testModifyMetaNewProduct(): void + { + $expectedMeta = include __DIR__ . '/../_files/eav_expected_meta_output_w_default.php'; + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/ImageAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/ImageAttributeTest.php new file mode 100644 index 0000000000000..75a20145fdcda --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/ImageAttributeTest.php @@ -0,0 +1,41 @@ +locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getProduct()); + $actualMeta = $this->eavModifier->modifyMeta([]); + $this->assertArrayNotHasKey('image_attribute', $this->getUsedAttributes($actualMeta)); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_image_attribute.php + * @return void + */ + public function testModifyMetaNewProduct(): void + { + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getNewProduct()); + $actualMeta = $this->eavModifier->modifyMeta([]); + $this->assertArrayNotHasKey('image_attribute', $this->getUsedAttributes($actualMeta)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/MultiselectAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/MultiselectAttributeTest.php new file mode 100644 index 0000000000000..eefb49dcf6239 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/MultiselectAttributeTest.php @@ -0,0 +1,65 @@ +callModifyMetaAndAssert( + $this->getProduct(), + $this->addMetaNesting($this->getAttributeMeta(), 'product-details', 'multiselect_attribute') + ); + } + + /** + * @return void + */ + public function testModifyData(): void + { + $product = $this->getProduct(); + $optionValue = $this->getOptionValueByLabel('multiselect_attribute', 'Option 3'); + $attributeData = ['multiselect_attribute' => $optionValue]; + $this->saveProduct($product, $attributeData); + $expectedData = $this->addDataNesting($attributeData); + $this->callModifyDataAndAssert($product, $expectedData); + } + + /** + * @return array + */ + private function getAttributeMeta(): array + { + return [ + 'dataType' => 'multiselect', + 'formElement' => 'multiselect', + 'visible' => '1', + 'required' => '0', + 'label' => 'Multiselect Attribute', + 'code' => 'multiselect_attribute', + 'source' => 'product-details', + 'scopeLabel' => '[GLOBAL]', + 'globalScope' => true, + 'sortOrder' => '__placeholder__', + 'options' => $this->getAttributeOptions('multiselect_attribute'), + 'componentType' => 'field', + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/SelectAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/SelectAttributeTest.php new file mode 100644 index 0000000000000..493608a43b77b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/SelectAttributeTest.php @@ -0,0 +1,66 @@ +callModifyMetaAndAssert( + $this->getProduct(), + $this->addMetaNesting($this->getAttributeMeta(), 'product-details', 'dropdown_attribute') + ); + } + + /** + * @return void + */ + public function testModifyData(): void + { + $product = $this->getProduct(); + $attributeData = [ + 'dropdown_attribute' => $this->getOptionValueByLabel('dropdown_attribute', 'Option 3') + ]; + $this->saveProduct($product, $attributeData); + $expectedData = $this->addDataNesting($attributeData); + $this->callModifyDataAndAssert($product, $expectedData); + } + + /** + * @return array + */ + private function getAttributeMeta(): array + { + return [ + 'dataType' => 'select', + 'formElement' => 'select', + 'visible' => '1', + 'required' => '0', + 'label' => 'Drop-Down Attribute', + 'code' => 'dropdown_attribute', + 'source' => 'product-details', + 'scopeLabel' => '[STORE VIEW]', + 'globalScope' => false, + 'sortOrder' => '__placeholder__', + 'options' => $this->getAttributeOptions('dropdown_attribute'), + 'componentType' => 'field', + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/VarcharAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/VarcharAttributeTest.php new file mode 100644 index 0000000000000..dfa40d138d640 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/VarcharAttributeTest.php @@ -0,0 +1,81 @@ +callModifyMetaAndAssert( + $this->getProduct(), + $this->addMetaNesting($this->getAttributeMeta(), 'product-details', 'varchar_attribute') + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_varchar_attribute.php + * @return void + */ + public function testModifyMetaNewProduct(): void + { + $this->setAttributeDefaultValue('varchar_attribute', 'test'); + $attributesMeta = array_merge($this->getAttributeMeta(), ['default' => 'test']); + $expectedMeta = $this->addMetaNesting( + $attributesMeta, + 'product-details', + 'varchar_attribute' + ); + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_varchar_attribute.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testModifyData(): void + { + $product = $this->getProduct(); + $attributeData = ['varchar_attribute' => 'Test message']; + $this->saveProduct($product, $attributeData); + $expectedData = $this->addDataNesting($attributeData); + $this->callModifyDataAndAssert($product, $expectedData); + } + + /** + * @return array + */ + private function getAttributeMeta(): array + { + return [ + 'dataType' => 'text', + 'formElement' => 'input', + 'visible' => '1', + 'required' => '0', + 'label' => 'Varchar Attribute', + 'code' => 'varchar_attribute', + 'source' => 'product-details', + 'scopeLabel' => '[GLOBAL]', + 'globalScope' => true, + 'sortOrder' => '__placeholder__', + 'componentType' => 'field' + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 6dd3436041b33..83c6e99df629e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -3,124 +3,218 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Eav\Model\AttributeSetRepository; +use Magento\TestFramework\Eav\Model\GetAttributeGroupByName; +use Magento\TestFramework\Eav\Model\ResourceModel\GetEntityIdByAttributeId; + /** + * Provides tests for eav modifier used in products admin form data provider. + * * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoAppArea adminhtml */ -class EavTest extends \PHPUnit\Framework\TestCase +class EavTest extends AbstractEavTest { /** - * @var \Magento\Framework\ObjectManagerInterface + * @var GetAttributeGroupByName */ - protected $objectManager; + private $attributeGroupByName; /** - * @var \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav + * @var GetEntityIdByAttributeId */ - protected $eavModifier; + private $getEntityIdByAttributeId; /** - * @var \Magento\Catalog\Model\Locator\LocatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AttributeSetRepository */ - protected $locatorMock; + private $setRepository; + /** + * @inheritdoc + */ protected function setUp() { - $mappings = [ - "text" => "input", - "hidden" => "input", - "boolean" => "checkbox", - "media_image" => "image", - "price" => "input", - "weight" => "input", - "gallery" => "image" - ]; - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->locatorMock = $this->createMock(\Magento\Catalog\Model\Locator\LocatorInterface::class); - $store = $this->objectManager->get(\Magento\Store\Api\Data\StoreInterface::class); - $this->locatorMock->expects($this->any())->method('getStore')->willReturn($store); - $this->eavModifier = $this->objectManager->create( - \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::class, - [ - 'locator' => $this->locatorMock, - 'formElementMapper' => $this->objectManager->create( - \Magento\Ui\DataProvider\Mapper\FormElement::class, - ['mappings' => $mappings] - ) - ] - ); parent::setUp(); + $this->attributeGroupByName = $this->objectManager->get(GetAttributeGroupByName::class); + $this->getEntityIdByAttributeId = $this->objectManager->get(GetEntityIdByAttributeId::class); + $this->setRepository = $this->objectManager->get(AttributeSetRepositoryInterface::class); } /** + * @magentoDataFixture Magento/Catalog/_files/product_text_attribute.php * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @dataProvider modifyMetaWithAttributeProvider + * @param string $groupName + * @param string $groupCode + * @param string $attributeCode + * @param array $attributeMeta + * @return void */ - public function testModifyMeta() - { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); - $product->load(1); - $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($product); - $expectedMeta = include __DIR__ . '/_files/eav_expected_meta_output.php'; + public function testModifyMetaWithAttributeInGroups( + string $groupName, + string $groupCode, + string $attributeCode, + array $attributeMeta + ): void { + $attributeGroup = $this->attributeGroupByName->execute($this->defaultSetId, $groupName); + $groupId = $attributeGroup ? $attributeGroup->getAttributeGroupId() : 'ynode-1'; + $data = [ + 'attributes' => [ + [$this->attributeRepository->get($attributeCode)->getAttributeId(), $groupId, 1], + ], + 'groups' => [ + [$groupId, $groupName, 1], + ], + ]; + $this->prepareAttributeSet($data); + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getProduct()); $actualMeta = $this->eavModifier->modifyMeta([]); + $expectedMeta = $this->addMetaNesting($attributeMeta, $groupCode, $attributeCode); $this->prepareDataForComparison($actualMeta, $expectedMeta); $this->assertEquals($expectedMeta, $actualMeta); } - public function testModifyMetaNewProduct() + /** + * @return array + */ + public function modifyMetaWithAttributeProvider(): array + { + $textAttributeMeta = [ + 'dataType' => 'textarea', + 'formElement' => 'textarea', + 'visible' => '1', + 'required' => '0', + 'label' => 'Text Attribute', + 'code' => 'text_attribute', + 'source' => 'content', + 'scopeLabel' => '[GLOBAL]', + 'globalScope' => true, + 'sortOrder' => '__placeholder__', + 'componentType' => 'field', + ]; + $urlKeyAttributeMeta = [ + 'dataType' => 'text', + 'formElement' => 'input', + 'visible' => '1', + 'required' => '0', + 'label' => 'URL Key', + 'code' => 'url_key', + 'source' => 'image-management', + 'scopeLabel' => '[STORE VIEW]', + 'globalScope' => false, + 'sortOrder' => '__placeholder__', + 'componentType' => 'field', + ]; + + return [ + 'new_attribute_in_existing_group' => [ + 'group_name' => 'Content', + 'group_code' => 'content', + 'attribute_code' => 'text_attribute', + 'attribute_meta' => $textAttributeMeta, + ], + 'new_attribute_in_new_group' => [ + 'group_name' => 'Test', + 'group_code' => 'test', + 'attribute_code' => 'text_attribute', + 'attribute_meta' => array_merge($textAttributeMeta, ['source' => 'test']), + ], + 'old_attribute_moved_to_existing_group' => [ + 'group_name' => 'Images', + 'group_code' => 'image-management', + 'attribute_code' => 'url_key', + 'attribute_meta' => $urlKeyAttributeMeta, + ], + 'old_attribute_moved_to_new_group' => [ + 'group_name' => 'Test', + 'group_code' => 'test', + 'attribute_code' => 'url_key', + 'attribute_meta' => array_merge($urlKeyAttributeMeta, ['source' => 'test']), + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testModifyMetaWithChangedGroupSorting(): void + { + $contentGroupId = $this->attributeGroupByName->execute($this->defaultSetId, 'Content') + ->getAttributeGroupId(); + $imagesGroupId = $this->attributeGroupByName->execute($this->defaultSetId, 'Images') + ->getAttributeGroupId(); + $additional = ['groups' => [[$contentGroupId, 'Content', 2], [$imagesGroupId, 'Images', 1]]]; + $this->prepareAttributeSet($additional); + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getProduct()); + $actualMeta = $this->eavModifier->modifyMeta([]); + $groupCodes = ['image-management', 'content']; + $groups = array_filter( + array_keys($actualMeta), + function ($group) use ($groupCodes) { + return in_array($group, $groupCodes); + } + ); + $this->assertEquals($groupCodes, $groups); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testModifyMetaWithRemovedGroup(): void { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); - $product->setAttributeSetId(4); - $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($product); - $expectedMeta = include __DIR__ . '/_files/eav_expected_meta_output_w_default.php'; + $designAttributes = ['page_layout', 'options_container', 'custom_layout_update']; + $designGroupId =$this->attributeGroupByName->execute($this->defaultSetId, 'Design') + ->getAttributeGroupId(); + $additional = ['removeGroups' => [$designGroupId]]; + $this->prepareAttributeSet($additional); + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getProduct()); $actualMeta = $this->eavModifier->modifyMeta([]); - $this->prepareDataForComparison($actualMeta, $expectedMeta); - $this->assertEquals($expectedMeta, $actualMeta); + $this->assertArrayNotHasKey('design', $actualMeta, 'Group "Design" still visible.'); + $this->assertEmpty( + array_intersect($designAttributes, $this->getUsedAttributes($actualMeta)), + 'Attributes from "Design" group still visible.' + ); } /** - * @magentoDataFixture Magento/Catalog/_files/product_simple_with_admin_store.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void */ - public function testModifyData() + public function testModifyMetaWithRemovedAttribute(): void { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); - $product->load(1); - $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($product); - $expectedData = include __DIR__ . '/_files/eav_expected_data_output.php'; - $actualData = $this->eavModifier->modifyData([]); - $this->prepareDataForComparison($actualData, $expectedData); - $this->assertEquals($expectedData, $actualData); + $attributeId = (int)$this->attributeRepository->get('meta_description')->getAttributeId(); + $entityAttributeId = $this->getEntityIdByAttributeId->execute($this->defaultSetId, $attributeId); + $additional = ['not_attributes' => [$entityAttributeId]]; + $this->prepareAttributeSet($additional); + $this->locatorMock->expects($this->any())->method('getProduct')->willReturn($this->getProduct()); + $actualMeta = $this->eavModifier->modifyMeta([]); + $this->assertArrayNotHasKey('meta_description', $this->getUsedAttributes($actualMeta)); } /** - * Prepare data for comparison to avoid false positive failures. - * - * Make sure that $data contains all the data contained in $expectedData, - * ignore all fields not declared in $expectedData + * Updates default attribute set. * - * @param array &$data - * @param array $expectedData + * @param array $additional * @return void */ - private function prepareDataForComparison(array &$data, array $expectedData) + private function prepareAttributeSet(array $additional): void { - foreach ($data as $key => &$item) { - if (!isset($expectedData[$key])) { - unset($data[$key]); - continue; - } - if ($item instanceof \Magento\Framework\Phrase) { - $item = (string)$item; - } elseif (is_array($item)) { - $this->prepareDataForComparison($item, $expectedData[$key]); - } elseif ($key === 'price_id' || $key === 'sortOrder') { - $data[$key] = '__placeholder__'; - } - } + $set = $this->setRepository->get($this->defaultSetId); + $data = [ + 'attributes' => [], + 'groups' => [], + 'not_attributes' => [], + 'removeGroups' => [], + 'attribute_set_name' => 'Default', + ]; + $set->organizeData(array_merge($data, $additional)); + $this->setRepository->save($set); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php new file mode 100644 index 0000000000000..ebfbd06d7edad --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php @@ -0,0 +1,187 @@ +locator = $this->getMockForAbstractClass(LocatorInterface::class); + $store = Bootstrap::getObjectManager()->create(StoreInterface::class); + $this->locator->method('getStore')->willReturn($store); + $this->modifier = Bootstrap::getObjectManager()->create(LayoutUpdate::class, ['locator' => $this->locator]); + $this->repo = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + $this->eavModifier = Bootstrap::getObjectManager()->create( + EavModifier::class, + [ + 'locator' => $this->locator, + 'formElementMapper' => Bootstrap::getObjectManager()->create( + \Magento\Ui\DataProvider\Mapper\FormElement::class, + [ + 'mappings' => [ + "text" => "input", + "hidden" => "input", + "boolean" => "checkbox", + "media_image" => "image", + "price" => "input", + "weight" => "input", + "gallery" => "image" + ] + ] + ) + ] + ); + $this->fakeFiles = Bootstrap::getObjectManager()->get(ProductLayoutUpdateManager::class); + } + + /** + * Test that data is being modified accordingly. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void + * @throws \Throwable + */ + public function testModifyData(): void + { + $product = $this->repo->get('simple'); + $this->locator->method('getProduct')->willReturn($product); + $product->setCustomAttribute('custom_layout_update', 'something'); + + $data = $this->modifier->modifyData([$product->getId() => ['product' => []]]); + $this->assertEquals( + LayoutUpdateAttribute::VALUE_USE_UPDATE_XML, + $data[$product->getId()]['product']['custom_layout_update_file'] + ); + } + + /** + * Extract options meta. + * + * @param array $meta + * @return array + */ + private function extractCustomLayoutOptions(array $meta): array + { + $this->assertArrayHasKey('design', $meta); + $this->assertArrayHasKey('children', $meta['design']); + $this->assertArrayHasKey('container_custom_layout_update_file', $meta['design']['children']); + $this->assertArrayHasKey('children', $meta['design']['children']['container_custom_layout_update_file']); + $this->assertArrayHasKey( + 'custom_layout_update_file', + $meta['design']['children']['container_custom_layout_update_file']['children'] + ); + $fieldMeta = $meta['design']['children']['container_custom_layout_update_file']['children']; + $fieldMeta = $fieldMeta['custom_layout_update_file']; + $this->assertArrayHasKey('arguments', $fieldMeta); + $this->assertArrayHasKey('data', $fieldMeta['arguments']); + $this->assertArrayHasKey('config', $fieldMeta['arguments']['data']); + $this->assertArrayHasKey('options', $fieldMeta['arguments']['data']['config']); + + return $fieldMeta['arguments']['data']['config']['options']; + } + + /** + * Check that entity specific options are returned. + * + * @return void + * @throws \Throwable + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testEntitySpecificData(): void + { + //Testing a category without layout xml + $product = $this->repo->get('simple'); + $this->locator->method('getProduct')->willReturn($product); + $this->fakeFiles->setFakeFiles((int)$product->getId(), ['testOne', 'test_two']); + + $meta = $this->eavModifier->modifyMeta([]); + $list = $this->extractCustomLayoutOptions($meta); + $expectedList = [ + [ + 'label' => 'No update', + 'value' => \Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate::VALUE_NO_UPDATE, + '__disableTmpl' => true + ], + ['label' => 'testOne', 'value' => 'testOne', '__disableTmpl' => true], + ['label' => 'test_two', 'value' => 'test_two', '__disableTmpl' => true] + ]; + sort($expectedList); + sort($list); + $this->assertEquals($expectedList, $list); + + //Product with old layout xml + $product->setCustomAttribute('custom_layout_update', 'test'); + $this->fakeFiles->setFakeFiles((int)$product->getId(), ['test3']); + + $meta = $this->eavModifier->modifyMeta([]); + $list = $this->extractCustomLayoutOptions($meta); + $expectedList = [ + [ + 'label' => 'No update', + 'value' => \Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate::VALUE_NO_UPDATE, + '__disableTmpl' => true + ], + [ + 'label' => 'Use existing', + 'value' => LayoutUpdateAttribute::VALUE_USE_UPDATE_XML, + '__disableTmpl' => true + ], + ['label' => 'test3', 'value' => 'test3', '__disableTmpl' => true], + ]; + sort($expectedList); + sort($list); + $this->assertEquals($expectedList, $list); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php index fa0a664738c1d..f6f4cc0e15159 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/eav_expected_meta_output.php @@ -40,9 +40,6 @@ "globalScope" => false, "code" => "status", "sortOrder" => "__placeholder__", - "service" => [ - "template" => "ui/form/element/helper/service" - ], "componentType" => "field" ], ], @@ -66,9 +63,6 @@ "globalScope" => false, "code" => "name", "sortOrder" => "__placeholder__", - "service" => [ - "template" => "ui/form/element/helper/service" - ], "componentType" => "field", "validation" => [ "required-entry" => true diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default.php new file mode 100644 index 0000000000000..d48578aa73465 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default.php @@ -0,0 +1,22 @@ +create(\Magento\Eav\Model\Entity\Attribute\Set::class); +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); +$defaultSetId = $objectManager->create(\Magento\Catalog\Model\Product::class)->getDefaultAttributeSetid(); +$data = [ + 'attribute_set_name' => 'new_attribute_set', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 300, +]; + +$attributeSet->setData($data); +$attributeSet->validate(); +$attributeSet->save(); +$attributeSet->initFromSkeleton($defaultSetId); +$attributeSet->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_rollback.php new file mode 100644 index 0000000000000..4cd18f6d23e8d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_rollback.php @@ -0,0 +1,21 @@ +create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); + +$attributeSetCollection = $objectManager->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class +)->create(); +$attributeSetCollection->addFilter('attribute_set_name', 'new_attribute_set'); +$attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); +$attributeSetCollection->setOrder('attribute_set_id'); +$attributeSetCollection->setPageSize(1); +$attributeSetCollection->load(); + +$attributeSet = $attributeSetCollection->fetchItem(); +$attributeSet->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php new file mode 100644 index 0000000000000..929b88367dd78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_set.php @@ -0,0 +1,26 @@ +create(\Magento\Eav\Model\Entity\Attribute\Set::class); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); +$defaultSetId = $objectManager->create(\Magento\Catalog\Model\Product::class)->getDefaultAttributeSetid(); + +$data = [ + 'attribute_set_name' => 'second_attribute_set', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 200, +]; + +$attributeSet->setData($data); +$attributeSet->validate(); +$attributeSet->save(); +$attributeSet->initFromSkeleton($defaultSetId); +$attributeSet->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group.php new file mode 100644 index 0000000000000..ccca2b7e4dbce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group.php @@ -0,0 +1,49 @@ +get(AttributeSetRepositoryInterface::class); +/** @var AttributeSetInterfaceFactory $attributeSetFactory */ +$attributeSetFactory = $objectManager->get(AttributeSetInterfaceFactory::class); +/** @var Type $entityType */ +$entityType = $objectManager->create(Type::class)->loadByCode(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attributeSet = $attributeSetFactory->create( + [ + 'data' => [ + 'id' => null, + 'attribute_set_name' => 'new_attribute_set', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 300, + ], + ] +); +$attributeSet->isObjectNew(true); +$attributeSet->setHasDataChanges(true); +$attributeSet->validate(); +$attributeSetRepository->save($attributeSet); +$attributeSet->initFromSkeleton($entityType->getDefaultAttributeSetId()); +/** @var AttributeGroupInterface $newGroup */ +$newGroup = $objectManager->get(GroupFactory::class)->create(); +$newGroup->setId(null) + ->setAttributeGroupName('Test attribute group name') + ->setAttributeSetId($attributeSet->getAttributeSetId()) + ->setSortOrder(11) + ->setAttributes([]); +/** @var AttributeGroupInterface[] $groups */ +$groups = $attributeSet->getGroups(); +array_push($groups, $newGroup); +$attributeSet->setGroups($groups); +$attributeSetRepository->save($attributeSet); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group_rollback.php new file mode 100644 index 0000000000000..f8628ea2d6ddb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_with_custom_group_rollback.php @@ -0,0 +1,21 @@ +get(GetAttributeSetByName::class); +/** @var AttributeSetRepositoryInterface $attributeSetRepository */ +$attributeSetRepository = $objectManager->get(AttributeSetRepositoryInterface::class); +$attributeSet = $getAttributeSetByName->execute('new_attribute_set'); + +if ($attributeSet) { + $attributeSetRepository->delete($attributeSet); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture.php new file mode 100644 index 0000000000000..6939031140523 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture.php @@ -0,0 +1,52 @@ +get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterface $attributeCountryOfManufacture */ +$attributeCountryOfManufacture = $attributeRepository->get('country_of_manufacture'); + +/** @var Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ +$attributeSet = $objectManager->create(Set::class); +/** @var Type $entityType */ +$entityType = $objectManager->create(Type::class) + ->loadByCode(Magento\Catalog\Model\Product::ENTITY); +$data = [ + 'attribute_set_name' => 'custom_attribute_set_wout_com', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 300, +]; + +$attributeSet->setData($data); +$attributeSet->validate(); +$attributeSet->save(); +$attributeSet->initFromSkeleton($entityType->getDefaultAttributeSetId()); +/** @var Group $group */ +foreach ($attributeSet->getGroups() as $group) { + $groupAttributes = $group->getAttributes(); + $newAttributes = array_filter( + $groupAttributes, + function ($attribute) use ($attributeCountryOfManufacture) { + /** @var ProductAttributeInterface $attribute */ + return (int)$attribute->getAttributeId() !== (int)$attributeCountryOfManufacture->getAttributeId(); + } + ); + if (count($newAttributes) < count($groupAttributes)) { + $group->setAttributes($newAttributes); + break; + } +} +$attributeSet->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture_rollback.php new file mode 100644 index 0000000000000..7b4719e151303 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_without_country_of_manufacture_rollback.php @@ -0,0 +1,58 @@ +create(AttributeSetRepositoryInterface::class); +/** @var Type $entityType */ +$entityType = $objectManager->create(Type::class) + ->loadByCode(Magento\Catalog\Model\Product::ENTITY); +$sortOrderBuilder = $objectManager->create(SortOrderBuilder::class); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter('attribute_set_name', 'custom_attribute_set_wout_com'); +$searchCriteriaBuilder->addFilter('entity_type_id', $entityType->getId()); +$attributeSetIdSortOrder = $sortOrderBuilder + ->setField('attribute_set_id') + ->setDirection(Collection::SORT_ORDER_DESC) + ->create(); +$searchCriteriaBuilder->addSortOrder($attributeSetIdSortOrder); +$searchCriteriaBuilder->setPageSize(1); +$searchCriteriaBuilder->setCurrentPage(1); + +/** @var AttributeSetSearchResults $searchResult */ +$searchResult = $attributeSetRepository->getList($searchCriteriaBuilder->create()); +$items = $searchResult->getItems(); + +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + if (count($items) > 0) { + /** @var Set $attributeSet */ + $attributeSet = reset($items); + $attributeSetRepository->deleteById($attributeSet->getId()); + } +} catch (\Exception $e) { + // In case of test run with DB isolation there is already no object in database + // since rollback fixtures called after transaction rollback. +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_image_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_image_attribute_rollback.php index 6dc8b60739f1f..626eb32a17051 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_image_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_image_attribute_rollback.php @@ -17,9 +17,10 @@ $attributeCollection->setCodeFilter('funny_image'); $attributeCollection->setEntityTypeFilter($entityType->getId()); $attributeCollection->setPageSize(1); -$attributeCollection->load(); -$attribute = $attributeCollection->fetchItem(); -$attribute->delete(); +$attribute = $attributeCollection->getFirstItem(); +if ($attribute->getId()) { + $attribute->delete(); +} // remove attribute set @@ -31,8 +32,9 @@ $attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); $attributeSetCollection->setOrder('attribute_set_id'); // descending is default value $attributeSetCollection->setPageSize(1); -$attributeSetCollection->load(); /** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ -$attributeSet = $attributeSetCollection->fetchItem(); -$attributeSet->delete(); +$attributeSet = $attributeSetCollection->getFirstItem(); +if ($attributeSet->getId()) { + $attributeSet->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_image.php new file mode 100644 index 0000000000000..934abffcb9c5b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_image.php @@ -0,0 +1,32 @@ +create(\Magento\Catalog\Model\Category::class); +$categoryParent->setName('Parent Image Category') + ->setPath('1/2') + ->setLevel(2) + ->setImage($filePath) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->save(); + +$categoryChild = $objectManager->create(\Magento\Catalog\Model\Category::class); +$categoryChild->setName('Child Image Category') + ->setPath($categoryParent->getPath()) + ->setLevel(3) + ->setImage($filePath) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(2) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_image_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_image_rollback.php new file mode 100644 index 0000000000000..baea438e9340c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_image_rollback.php @@ -0,0 +1,24 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +$collection + ->addAttributeToFilter('name', ['in' => ['Parent Image Category', 'Child Image Category']]) + ->load() + ->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index a5ab961932461..25bb55ffbc32c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -64,6 +64,7 @@ ->setIsActive(true) ->setIsAnchor(true) ->setPosition(1) + ->setDescription('Category 1.1 description.') ->save(); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); @@ -79,6 +80,7 @@ ->setPosition(1) ->setCustomUseParentSettings(0) ->setCustomDesign('Magento/blank') + ->setDescription('This is the description for Category 1.1.1') ->save(); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_disabled.php new file mode 100644 index 0000000000000..26f4565d70b37 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_disabled.php @@ -0,0 +1,34 @@ +create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(59) + ->setName('Category 1.1.1.1') + ->setParentId(5) + ->setPath('1/2/3/4/5/59') + ->setLevel(5) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setCustomUseParentSettings(0) + ->setCustomDesign('Magento/blank') + ->setDescription('This is the description for Category 1.1.1.1') + ->save(); + +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); + +// Category 1.1.1 +$category->load(4); +$category->setIsActive(false); +$category->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_disabled_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_disabled_rollback.php new file mode 100644 index 0000000000000..cc42bd6a09753 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories_disabled_rollback.php @@ -0,0 +1,24 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +foreach ($collection->addAttributeToFilter('level', ['in' => [59]]) as $category) { + /** @var \Magento\Catalog\Model\Category $category */ + $category->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php index 37398abdb8f7d..29b4a05c4dcbe 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php @@ -9,5 +9,6 @@ ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $attribute->setAttributeCode('test_attribute_code_666') ->setEntityTypeId(3) - ->setIsGlobal(1); + ->setIsGlobal(1) + ->setIsUserDefined(1); $attribute->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_different_price_products.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_different_price_products.php new file mode 100644 index 0000000000000..2e87e1e820f86 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_different_price_products.php @@ -0,0 +1,68 @@ +get(StoreManagerInterface::class); +$categoryFactory = $objectManager->get(CategoryInterfaceFactory::class); +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +$currentStoreId = $storeManager->getStore()->getId(); + +$storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); +$category = $categoryFactory->create(); +$category->isObjectNew(true); +$category->setName('Category 999') + ->setParentId(2) + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1); +$category = $categoryRepository->save($category); +$storeManager->setCurrentStore($currentStoreId); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setStoreId(Store::DEFAULT_STORE_ID) + ->setWebsiteIds([1]) + ->setName('Simple Product With Price 10') + ->setSku('simple1000') + ->setPrice(10) + ->setWeight(1) + ->setStockData(['use_config_manage_stock' => 0]) + ->setCategoryIds([$category->getId()]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +$productRepository->save($product); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setStoreId(Store::DEFAULT_STORE_ID) + ->setWebsiteIds([1]) + ->setName('Simple Product With Price 20') + ->setSku('simple1001') + ->setPrice(20) + ->setWeight(1) + ->setStockData(['use_config_manage_stock' => 0]) + ->setCategoryIds([$category->getId()]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_different_price_products_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_different_price_products_rollback.php new file mode 100644 index 0000000000000..4658a5b9bc8af --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_different_price_products_rollback.php @@ -0,0 +1,46 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$categoryCollectionFactory = $objectManager->get(CollectionFactory::class); +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +try { + $productRepository->deleteById('simple1000'); +} catch (NoSuchEntityException $e) { + //Already deleted. +} + +try { + $productRepository->deleteById('simple1001'); +} catch (NoSuchEntityException $e) { + //Already deleted. +} + +try { + $categoryCollection = $categoryCollectionFactory->create(); + $category = $categoryCollection + ->addAttributeToFilter(CategoryInterface::KEY_NAME, 'Category 999') + ->setPageSize(1) + ->getFirstItem(); + $categoryRepository->delete($category); +} catch (NoSuchEntityException $e) { + //Already deleted. +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_two_products.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_two_products.php new file mode 100644 index 0000000000000..31557fbe9a748 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_two_products.php @@ -0,0 +1,15 @@ +create(CategoryLinkManagementInterface::class); +$categoryLinkManagement->assignProductToCategories('simple2', [333]); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_two_products_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_two_products_rollback.php new file mode 100644 index 0000000000000..7500a92d1614c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_two_products_rollback.php @@ -0,0 +1,9 @@ +get(\Magento\Eav\Model\Config::class); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +$attribute->setIsSearchable(1) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); + +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); +$attributeRepository->save($attribute); +CacheCleaner::cleanAll(); +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php new file mode 100644 index 0000000000000..49e2a8e88a1ac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation_rollback.php @@ -0,0 +1,9 @@ + 0, 'frontend_label' => ['Drop-Down Attribute'], 'backend_type' => 'varchar', - 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, 'option' => [ 'value' => [ 'option_1' => ['Option 1'], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php new file mode 100644 index 0000000000000..0ed7317762056 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php @@ -0,0 +1,18 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' +); +$attribute->load('dropdown_attribute', 'attribute_code'); +$attribute->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/inactive_category.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/inactive_category.php new file mode 100644 index 0000000000000..e604d4e9f061b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/inactive_category.php @@ -0,0 +1,28 @@ +create(CategoryResource::class); +/** @var Category $category */ +$category = $objectManager->get(CategoryFactory::class)->create(); +$category->isObjectNew(true); +$data = [ + 'entity_id' => 111, + 'path' => '1/2/111', + 'name' => 'Test Category', + 'attribute_set_id' => $category->getDefaultAttributeSetId(), + 'parent_id' => 2, + 'is_active' => false, + 'include_in_menu' => true, +]; +$category->setData($data); +$categoryResource->save($category); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/inactive_category_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/inactive_category_rollback.php new file mode 100644 index 0000000000000..706a0bd9d05b0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/inactive_category_rollback.php @@ -0,0 +1,28 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); + +try { + $category = $categoryRepository->get(111); + $categoryRepository->delete($category); +} catch (NoSuchEntityException $e) { + //category already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image.gif b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image.gif new file mode 100755 index 0000000000000..82bccf7ba2fa6 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image.gif differ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_related_products.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_related_products.php new file mode 100644 index 0000000000000..afd8d76a92b13 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_related_products.php @@ -0,0 +1,68 @@ +get(ProductFactory::class); +/** @var ProductLinkInterfaceFactory $linkFactory */ +$linkFactory = Bootstrap::getObjectManager()->get(ProductLinkInterfaceFactory::class); + +$rootProductCount = 10; +$rootSku = 'simple-related-'; +$simpleProducts = []; +for ($i =1; $i <= $rootProductCount; $i++) { + /** @var Product $product */ + $product = $factory->create(); + $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Related Product #' .$i) + ->setSku($rootSku .$i) + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->save(); + $simpleProducts[$i] = $product; +} + +$linkTypes = ['crosssell', 'related', 'upsell']; +$linkedMaxCount = 10; +foreach ($simpleProducts as $simpleI => $product) { + $linkedCount = rand(1, $linkedMaxCount); + $links = []; + for ($i = 0; $i < $linkedCount; $i++) { + /** @var Product $linkedProduct */ + $linkedProduct = $factory->create(); + $linkedSku = 'related-product-' .$simpleI .'-' .$i; + $linkedProduct->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Related product #' .$simpleI .'-' .$i) + ->setSku($linkedSku) + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->save(); + /** @var ProductLinkInterface $link */ + $link = $linkFactory->create(); + $link->setSku($product->getSku()); + $link->setLinkedProductSku($linkedSku); + $link->setPosition($i + 1); + $link->setLinkType($linkTypes[rand(0, count($linkTypes) - 1)]); + $links[] = $link; + } + $product->setProductLinks($links); + $product->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_related_products_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_related_products_rollback.php new file mode 100644 index 0000000000000..2e728efc103c2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_related_products_rollback.php @@ -0,0 +1,34 @@ +get(ProductRepository::class); +/** @var SearchCriteriaBuilder $criteriaBuilder */ +$criteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$listToDelete = $repo->getList($criteriaBuilder->addFilter('sku', $rootSku, 'like')->create()); +foreach ($listToDelete->getItems() as $item) { + try { + $repo->delete($item); + } catch (\Throwable $exception) { + //Could be deleted before + } +} +$listToDelete = $repo->getList($criteriaBuilder->addFilter('sku', $linkedSku, 'like')->create()); +foreach ($listToDelete->getItems() as $item) { + try { + $repo->delete($item); + } catch (\Throwable $exception) { + //Could be deleted before + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_attribute.php index b5187d5d80768..b59675c5a28ae 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_attribute.php @@ -10,5 +10,6 @@ $attribute->setAttributeCode('test_attribute_code_333') ->setEntityTypeId(4) ->setIsGlobal(1) - ->setPrice(95); + ->setPrice(95) + ->setIsUserDefined(1); $attribute->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php new file mode 100644 index 0000000000000..34dccc2284445 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php @@ -0,0 +1,50 @@ +get(AttributeRepositoryInterface::class); +/** @var Attribute $attribute */ +$attribute = $objectManager->create(Attribute::class); +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); + +$attribute->setData( + [ + 'attribute_code' => 'boolean_attribute', + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'boolean', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Boolean Attribute'], + 'backend_type' => 'int', + 'source_model' => Boolean::class + ] +); + +$attributeRepository->save($attribute); + +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute_rollback.php new file mode 100644 index 0000000000000..c234eb91c84a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute_rollback.php @@ -0,0 +1,21 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var Attribute $attribute */ +$attribute = $objectManager->create(Attribute::class); +$attribute->load('boolean_attribute', 'attribute_code'); +$attribute->delete(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_date_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_date_attribute.php new file mode 100644 index 0000000000000..43ea543a5909e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_date_attribute.php @@ -0,0 +1,51 @@ +create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +if (!$attribute->loadByCode($entityType, 'date_attribute')->getAttributeId()) { + $attribute->setData( + [ + 'attribute_code' => 'date_attribute', + 'entity_type_id' => $entityType, + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'date', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Date Attribute'], + 'backend_type' => 'datetime', + ] + ); + $attributeRepository->save($attribute); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'General', + $attribute->getId() + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_date_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_date_attribute_rollback.php new file mode 100644 index 0000000000000..b20da89be0136 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_date_attribute_rollback.php @@ -0,0 +1,25 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); + +try { + $attributeRepository->deleteById('date_attribute'); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_decimal_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_decimal_attribute.php new file mode 100644 index 0000000000000..949475607d773 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_decimal_attribute.php @@ -0,0 +1,52 @@ +create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +if (!$attribute->loadByCode($entityType, 'decimal_attribute')->getAttributeId()) { + $attribute->setData( + [ + 'attribute_code' => 'decimal_attribute', + 'entity_type_id' => $entityType, + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'price', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Decimal Attribute'], + 'backend_type' => 'decimal', + 'backend_model' => Price::class, + ] + ); + $attributeRepository->save($attribute); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'General', + $attribute->getId() + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_decimal_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_decimal_attribute_rollback.php new file mode 100644 index 0000000000000..de187379bc99a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_decimal_attribute_rollback.php @@ -0,0 +1,25 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); + +try { + $attributeRepository->deleteById('decimal_attribute'); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_dropdown_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_dropdown_attribute.php new file mode 100644 index 0000000000000..ae0c1d3613380 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_dropdown_attribute.php @@ -0,0 +1,67 @@ +create(Attribute::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); +$entityTypeId = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); + +if (!$attribute->loadByCode($entityTypeId, 'dropdown_attribute')->getId()) { + $attribute->setData( + [ + 'attribute_code' => 'dropdown_attribute', + 'entity_type_id' => $entityTypeId, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Drop-Down Attribute'], + 'backend_type' => 'int', + 'option' => [ + 'value' => [ + 'option_1' => ['Option 1'], + 'option_2' => ['Option 2'], + 'option_3' => ['Option 3'], + ], + 'order' => [ + 'option_1' => 1, + 'option_2' => 2, + 'option_3' => 3, + ], + ], + ] + ); + $attributeRepository->save($attribute); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'Attributes', + $attribute->getId() + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_dropdown_attribute_rollback.php new file mode 100644 index 0000000000000..b48acc0ca0ac6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_dropdown_attribute_rollback.php @@ -0,0 +1,25 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); + +try { + $attributeRepository->deleteById('dropdown_attribute'); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image_attribute.php new file mode 100644 index 0000000000000..d8f0299f2a876 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image_attribute.php @@ -0,0 +1,51 @@ +create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +if (!$attribute->loadByCode($entityType, 'image_attribute')->getAttributeId()) { + $attribute->setData( + [ + 'attribute_code' => 'image_attribute', + 'entity_type_id' => $entityType, + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'media_image', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Image Attribute'], + 'backend_type' => 'varchar', + ] + ); + $attributeRepository->save($attribute); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'General', + $attribute->getId() + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image_attribute_rollback.php new file mode 100644 index 0000000000000..c9d28686741b8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image_attribute_rollback.php @@ -0,0 +1,25 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); + +try { + $attributeRepository->deleteById('image_attribute'); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multistore_different_short_description.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multistore_different_short_description.php new file mode 100644 index 0000000000000..4f0d1ce6d47ff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multistore_different_short_description.php @@ -0,0 +1,47 @@ +create(ProductRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); + +$currentStoreId = $storeManager->getStore()->getId(); +$secondStoreId = $storeManager->getStore('fixturestore')->getId(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$storeManager->getWebsite(true)->getId()]) + ->setName('Simple Product One') + ->setSku('simple-different-short-description') + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setShortDescription('First store view short description') + ->setStatus(Status::STATUS_ENABLED); +$productRepository->save($product); + +try { + $storeManager->setCurrentStore($secondStoreId); + $product = $productRepository->get('simple-different-short-description', false, $secondStoreId); + $product->setShortDescription('Second store view short description'); + $productRepository->save($product); +} finally { + $storeManager->setCurrentStore($currentStoreId); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multistore_different_short_description_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multistore_different_short_description_rollback.php new file mode 100644 index 0000000000000..4191b9ff00acc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multistore_different_short_description_rollback.php @@ -0,0 +1,29 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple-different-short-description'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/core_fixturestore_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_multistore_with_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_multistore_with_url_key.php new file mode 100644 index 0000000000000..82a1cd4b98e35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_multistore_with_url_key.php @@ -0,0 +1,50 @@ +create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setStoreId(Store::DEFAULT_STORE_ID) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product 100') + ->setSku('simple_100') + ->setUrlKey('url-key') + ->setPrice(10) + ->setWeight(1) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +/** @var StockItemInterface $stockItem */ +$stockItem = $objectManager->create(StockItemInterface::class); +$stockItem->setQty(100) + ->setIsInStock(true); +$extensionAttributes = $product->getExtensionAttributes(); +$extensionAttributes->setStockItem($stockItem); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->save($product); + +$product->setStoreId(Store::DISTRO_STORE_ID) + ->setName('StoreTitle') + ->setUrlKey('url-key'); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_multistore_with_url_key_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_multistore_with_url_key_rollback.php new file mode 100644 index 0000000000000..7130a7c4a5612 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_multistore_with_url_key_rollback.php @@ -0,0 +1,22 @@ +get(ProductRepositoryInterface::class); +try { + $productRepository->deleteById('simple_100'); +} catch (NoSuchEntityException $e) { + //Entity already deleted +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php index 6630c0d69e34f..a0e4369b986e4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock.php @@ -20,7 +20,7 @@ $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setWebsiteIds([1]) - ->setName('Simple Product') + ->setName('Simple Product Out Of Stock') ->setSku('simple-out-of-stock') ->setPrice(10) ->setWeight(1) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php new file mode 100644 index 0000000000000..6ae4af61bc51a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php @@ -0,0 +1,52 @@ +reinitialize(); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription('Short description') + ->setTaxClassId(0) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php new file mode 100644 index 0000000000000..4c858f322f8a7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php @@ -0,0 +1,8 @@ +get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $productFactory->create(); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product Tax None') + ->setSku('simple-product-tax-none') + ->setPrice(205) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1 + ] + ); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_tax_none_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_tax_none_rollback.php new file mode 100644 index 0000000000000..ceffb1c87d970 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_tax_none_rollback.php @@ -0,0 +1,33 @@ +get(ProductRepositoryInterface::class); +/** @var \Magento\Framework\Registry $registry */ +$registry =$objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + /** @var ProductInterface $product */ + $product = $productRepository->get('simple-product-tax-none', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + // isolation on +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_country_of_manufacture.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_country_of_manufacture.php new file mode 100644 index 0000000000000..fd09b8bd1f0f2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_country_of_manufacture.php @@ -0,0 +1,49 @@ +reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productFactory = $objectManager->create(ProductInterfaceFactory::class); +/** @var $product \Magento\Catalog\Model\Product */ + +$product = $productFactory->create(); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product With Country Of Manufacture') + ->setSku('simple_with_com') + ->setPrice(10) + ->setWeight(1) + ->setCountryOfManufacture('AO') + ->setShortDescription('Short description') + ->setTaxClassId(0) + ->setDescription('Description') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_country_of_manufacture_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_country_of_manufacture_rollback.php new file mode 100644 index 0000000000000..ffeb7eb143410 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_country_of_manufacture_rollback.php @@ -0,0 +1,32 @@ +create(ProductRepositoryInterface::class); + +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + /** @var Product $product */ + $product = $productRepository->get('simple_with_com'); + $productRepository->delete($product); +} catch (\Exception $e) { + // In case of test run with DB isolation there is already no object in database + // since rollback fixtures called after transaction rollback. +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_file_option.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_file_option.php new file mode 100644 index 0000000000000..1cd36e7f4726f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_file_option.php @@ -0,0 +1,85 @@ +create(CategoryLinkManagementInterface::class); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple_with_custom_file_option') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription('Short description') + ->setTaxClassId(0) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ) + ->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +$option = [ + 'title' => 'file option', + 'type' => 'file', + 'is_require' => true, + 'sort_order' => 1, + 'price' => 30.0, + 'price_type' => 'percent', + 'sku' => 'sku3', + 'file_extension' => 'jpg, png, gif', + 'image_size_x' => 100, + 'image_size_y' => 100, +]; + +$customOptions = []; + +/** @var ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->create(ProductCustomOptionInterfaceFactory::class); +/** @var ProductCustomOptionInterface $customOption */ +$customOption = $customOptionFactory->create(['data' => $option]); +$customOption->setProductSku($product->getSku()); +$customOptions[] = $customOption; + +$product->setOptions($customOptions); + +/** @var ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_file_option_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_file_option_rollback.php new file mode 100644 index 0000000000000..87321b2a080c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_file_option_rollback.php @@ -0,0 +1,31 @@ +getInstance()->reinitialize(); + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple_with_custom_file_option', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_date_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_date_attribute.php new file mode 100644 index 0000000000000..28a89ff883c4f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_date_attribute.php @@ -0,0 +1,61 @@ +setData('is_used_for_promo_rules', 1); + +/** @var \Magento\Catalog\Model\ProductFactory $productFactory */ +$productFactory = $objectManager->get(Magento\Catalog\Model\ProductFactory::class); +$product = $productFactory->create(); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product with date') + ->setSku('simple_with_date') + ->setPrice(10) + ->setDescription('Description with html tag') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setDateAttribute(date('Y-m-d')) + ->setUrlKey('simple_with_date') + ->save(); + +$product2 = $productFactory->create(); +$product2->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product with date -1') + ->setSku('simple_with_date2') + ->setPrice(10) + ->setDescription('Description with html tag') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setDateAttribute(date('Y-m-d', strtotime(date('Y-m-d') . '-1 day'))) + ->setUrlKey('simple_with_date2') + ->save(); + +$product3 = $productFactory->create(); +$product3->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product with date +1') + ->setSku('simple_with_date3') + ->setPrice(10) + ->setDescription('Description with html tag') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setDateAttribute(date('Y-m-d', strtotime(date('Y-m-d') . '+1 day'))) + ->setUrlKey('simple_with_date3') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_date_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_date_attribute_rollback.php new file mode 100644 index 0000000000000..da61eb1d2332b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_date_attribute_rollback.php @@ -0,0 +1,44 @@ +getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple_with_date', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +try { + $product = $productRepository->get('simple_with_date2', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +try { + $product = $productRepository->get('simple_with_date3', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +include __DIR__ . '/product_date_attribute_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php index 23fd8d7fe324e..928c036e8fb40 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php @@ -41,7 +41,7 @@ $productRepository->save($product); } catch (\Exception $e) { // problems during save -}; +} /** @var ProductInterface $product */ $product = $objectManager->create(ProductInterface::class); @@ -60,4 +60,4 @@ $productRepository->save($product); } catch (\Exception $e) { // problems during save -}; +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php index 93c7ba61c6f49..67288bec86ad5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php @@ -106,6 +106,15 @@ 'sort_order' => 2, ], ], + ], + [ + 'title' => 'date option', + 'type' => 'date', + 'price' => 80.0, + 'price_type' => 'fixed', + 'sku' => 'date option sku', + 'is_require' => false, + 'sort_order' => 6 ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_system_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_system_attribute.php new file mode 100644 index 0000000000000..1e7429cc831f5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_system_attribute.php @@ -0,0 +1,16 @@ +get(\Magento\Catalog\Model\Product\Attribute\Repository::class); +/** @var $attribute \Magento\Eav\Api\Data\AttributeInterface */ +$attribute = $attributeRepository->get('test_attribute_code_333'); + +$attributeRepository->save($attribute->setIsUserDefined(0)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_system_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_system_attribute_rollback.php new file mode 100644 index 0000000000000..0f997ff4b4941 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_system_attribute_rollback.php @@ -0,0 +1,37 @@ +get(\Magento\Catalog\Model\Product\Attribute\Repository::class); + +try { + /** @var $attribute \Magento\Eav\Api\Data\AttributeInterface */ + $attribute = $attributeRepository->get('test_attribute_code_333'); + $attributeRepository->save($attribute->setIsUserDefined(1)); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch +} catch (NoSuchEntityException $e) { +} +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $attribute = $attributeRepository->get('test_attribute_code_333'); + if ($attribute->getId()) { + $attribute->delete(); + } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch +} catch (\Exception $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_two_websites.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_two_websites.php new file mode 100644 index 0000000000000..d7150d7ec41b1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_two_websites.php @@ -0,0 +1,40 @@ +create(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->create(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId, $websiteId]) + ->setName('Simple Product on two websites') + ->setSku('simple-on-two-websites') + ->setPrice(10) + ->setDescription('Description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_two_websites_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_two_websites_rollback.php new file mode 100644 index 0000000000000..09cafb990a910 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_two_websites_rollback.php @@ -0,0 +1,29 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + +try { + $productRepository->deleteById('simple-on-two-websites'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/second_website_with_two_stores_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_varchar_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_varchar_attribute.php new file mode 100644 index 0000000000000..d23acfcdd196e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_varchar_attribute.php @@ -0,0 +1,51 @@ +create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +if (!$attribute->loadByCode($entityType, 'varchar_attribute')->getAttributeId()) { + $attribute->setData( + [ + 'attribute_code' => 'varchar_attribute', + 'entity_type_id' => $entityType, + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'text', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Varchar Attribute'], + 'backend_type' => 'varchar', + ] + ); + $attribute->save(); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'General', + $attribute->getId() + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_varchar_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_varchar_attribute_rollback.php new file mode 100644 index 0000000000000..c238803a77108 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_varchar_attribute_rollback.php @@ -0,0 +1,25 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); + +try { + $attributeRepository->deleteById('varchar_attribute'); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types.php new file mode 100644 index 0000000000000..4176e14209edb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types.php @@ -0,0 +1,30 @@ +create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +$imageData = [ + 'file' => '/m/a/magento_image.jpg', + 'position' => 1, + 'label' => 'Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' +]; + +/** @var $product Product */ +$product->setStoreId(0) + ->setData('media_gallery', ['images' => [$imageData]]) + ->setCanSaveCustomOptions(true) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types_rollback.php new file mode 100644 index 0000000000000..2033f092b3979 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types_rollback.php @@ -0,0 +1,9 @@ +get(ProductRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var GetAttributeSetByName $attributeSet */ +$attributeSet = $objectManager->get(GetAttributeSetByName::class); +$customAttributeSet = $attributeSet->execute('new_attribute_set'); +$product = $productFactory->create(); +$product + ->setTypeId('simple') + ->setAttributeSetId($customAttributeSet->getAttributeSetId()) + ->setWebsiteIds([1]) + ->setStoreId(Store::DEFAULT_STORE_ID) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1]) + ->setQty(22); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_test_attribute_set_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_test_attribute_set_rollback.php new file mode 100644 index 0000000000000..1ec341cd4fd58 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_test_attribute_set_rollback.php @@ -0,0 +1,33 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = $objectManager->get(StockRegistryStorage::class); +try { + $product = $productRepository->get('simple'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already deleted. +} +$stockRegistryStorage->clean(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/attribute_set_based_on_default_with_custom_group_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php new file mode 100644 index 0000000000000..85b3146fc7ec0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting.php @@ -0,0 +1,112 @@ +get(ProductRepositoryInterface::class); +$categoryLinkRepository = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + [ + 'productRepository' => $productRepository + ] +); +$categoryLinkManagement = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkManagementInterface::class, + [ + 'productRepository' => $productRepository, + 'categoryLinkRepository' => $categoryLinkRepository + ] +); +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId( + 330 +)->setCreatedAt( + '2019-08-27 11:05:07' +)->setName( + 'Colorful Category' +)->setParentId( + 2 +)->setPath( + '1/2/330' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->save(); + +$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) + ->getEntityType('catalog_product') + ->getDefaultAttributeSetId(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('Navy Blue Striped Shoes') + ->setSku('navy-striped-shoes') + ->setPrice(40) + ->setWeight(8) + ->setDescription('blue striped flip flops at one') + ->setMetaTitle('navy blue colored shoes meta title') + ->setMetaKeyword('blue, navy, striped , women, kids') + ->setMetaDescription('blue shoes women kids meta description') + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('light green Shoes') + ->setSku('light-green-shoes') + ->setPrice(40) + ->setWeight(8) + ->setDescription('green polka dots shoes one') + ->setMetaTitle('light green shoes meta title') + ->setMetaKeyword('light, green , women, kids') + ->setMetaDescription('shoes women kids meta description') + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); + +/** @var \Magento\Catalog\Model\Product $greyProduct */ +$greyProduct = $productRepository->get('grey_shorts'); +$greyProduct->setDescription('Description with Blue lines'); +$productRepository->save($greyProduct); + +$skus = ['green_socks', 'white_shorts','red_trousers','blue_briefs','grey_shorts', + 'navy-striped-shoes', 'light-green-shoes']; + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [330] + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php new file mode 100644 index 0000000000000..5a1dd30c6b492 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php @@ -0,0 +1,35 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->load(330); +if ($category->getId()) { + $category->delete(); +} +// Remove products +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsToDelete = ['green_socks', 'white_shorts','red_trousers','blue_briefs', + 'grey_shorts', 'navy-striped-shoes','light-green-shoes']; + +foreach ($productsToDelete as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell.php index 8df969a31019c..489666517419f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +require __DIR__ . '/products_upsell_rollback.php'; + /** @var $product \Magento\Catalog\Model\Product */ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); $product->setTypeId( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell_rollback.php index a9c25dec58547..633eef3371a21 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_upsell_rollback.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\CatalogInventory\Model\StockRegistryStorage; + /** @var \Magento\Framework\Registry $registry */ $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); @@ -25,3 +28,8 @@ $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); + +/** @var StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(StockRegistryStorage::class); +$stockRegistryStorage->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_boolean_attribute.php new file mode 100644 index 0000000000000..65c8c5a251881 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_boolean_attribute.php @@ -0,0 +1,35 @@ +get(ProductRepositoryInterface::class); + +$yesIds = [101, 102, 104]; +$noIds = [103, 105]; + +foreach ($yesIds as $id) { + $product = $productRepository->getById($id); + $product->setBooleanAttribute(1); + $productRepository->save($product); +} +foreach ($noIds as $id) { + $product = $productRepository->getById($id); + $product->setBooleanAttribute(0); + $productRepository->save($product); +} +CacheCleaner::cleanAll(); +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = $objectManager->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_boolean_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_boolean_attribute_rollback.php new file mode 100644 index 0000000000000..8a70aead1f36d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_boolean_attribute_rollback.php @@ -0,0 +1,8 @@ +get(StoreManagerInterface::class); +/** @var StoreInterface $store */ +$store = $storeManager->getStore(); +$eavConfig = $objectManager->get(EavConfig::class); +$eavConfig->clear(); +$attribute = $eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'dropdown_without_default'); +/** @var CategorySetup $installer */ +$installer = $objectManager->get(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE, 'Default'); + +/** @var ProductInterface $product */ +$product = $objectManager->get(ProductInterface::class); +$product->setTypeId(ProductType::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setName('Simple Product1') + ->setSku('test_attribute_dropdown_without_default') + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->save($product); + +if (!$attribute->getId()) { + /** @var $attribute */ + $attribute = $objectManager->get(EavAttribute::class); + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); + $attribute->setData( + [ + 'attribute_code' => 'dropdown_without_default', + 'entity_type_id' => $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE), + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + $attributeRepository->save($attribute); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'General', + $attribute->getId() + ); +} +/** @var AttributeOptionManagementInterface $options */ +$attributeOption = $objectManager->get(AttributeOptionManagementInterface::class); +/* Getting the first nonempty option */ +/** @var AttributeOptionInterface $option */ +$option = $attributeOption->getItems($attribute->getEntityTypeId(), $attribute->getAttributeCode())[1]; +$product->setStoreId($store->getId()) + ->setData('dropdown_without_default', $option->getValue()); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view_rollback.php new file mode 100644 index 0000000000000..a60588c16ab62 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view_rollback.php @@ -0,0 +1,48 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$eavConfig = $objectManager->get(EavConfig::class); +$eavConfig->clear(); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + /** @var AttributeInterface $attribute */ + $attribute = $attributeRepository->get(ProductAttributeInterface::ENTITY_TYPE_CODE, 'dropdown_without_default'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $e) { + //Attribute already deleted +} +try { + /** @var ProductInterface $product */ + $product = $productRepository->get('test_attribute_dropdown_without_default'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already deleted +} +$objectManager->get(ProductEav::class)->executeRow($product->getId()); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 48c47c9988d59..7bee46bc2078f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -36,7 +36,7 @@ 'is_unique' => 0, 'is_required' => 0, 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 0, + 'is_visible_in_advanced_search' => 1, 'is_comparable' => 1, 'is_filterable' => 1, 'is_filterable_in_search' => 1, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php new file mode 100644 index 0000000000000..72336c48410d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -0,0 +1,152 @@ +get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default_value' => 'option_0' + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} +// create a second attribute +if (!$attribute1->getId()) { + + /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute1 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute1->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute1); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + 'catalog_product', + $attributeSet->getId(), + $attributeSet->getDefaultGroupId(), + $attribute1->getId() + ); +} + +$eavConfig->clear(); + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; + +foreach ($productsWithNewAttributeSet as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $product->setAttributeSetId($attributeSet->getId()); + $product->setStockData( + ['use_config_manage_stock' => 1, + 'qty' => 50, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1] + ); + $productRepository->save($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + + } +} +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php new file mode 100644 index 0000000000000..5cababbc988c7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php @@ -0,0 +1,46 @@ +get(\Magento\Eav\Model\Config::class); +$attributesToDelete = ['test_configurable', 'second_test_configurable']; +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); + +foreach ($attributesToDelete as $attributeCode) { + /** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ + $attribute = $attributeRepository->get('catalog_product', $attributeCode); + $attributeRepository->delete($attribute); +} +/** @var $product \Magento\Catalog\Model\Product */ +$objectManager = Bootstrap::getObjectManager(); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); + +// remove attribute set + +/** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attributeSetCollection */ +$attributeSetCollection = $objectManager->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::class +); +$attributeSetCollection->addFilter('attribute_set_name', 'second_attribute_set'); +$attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); +$attributeSetCollection->setOrder('attribute_set_id'); // descending is default value +$attributeSetCollection->setPageSize(1); +$attributeSetCollection->load(); + +/** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ +$attributeSet = $attributeSetCollection->fetchItem(); +$attributeSet->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php new file mode 100644 index 0000000000000..7d4f22e154030 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php @@ -0,0 +1,98 @@ +create( + \Magento\Catalog\Setup\CategorySetup::class +); + +/** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ +$options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class +); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = $eavConfig->getAttribute('catalog_product', 'multiselect_attribute'); + +$eavConfig->clear(); +$attribute->setIsSearchable(1) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); +$attributeRepository->save($attribute); + +$options->setAttributeFilter($attribute->getId()); +$optionIds = $options->getAllIds(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[0] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1 and 2') + ->setSku('simple_ms_1') + ->setPrice(10) + ->setDescription('Hello " &" Bring the water bottle when you can!') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[1],$optionIds[2]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[1] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 2 and 3') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->save($product); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[2] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1 and 3') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php new file mode 100644 index 0000000000000..eb8201f04e6cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php @@ -0,0 +1,32 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create('Magento\Catalog\Model\Product') + ->getCollection(); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(IndexerRegistry::class) + ->get(Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID) + ->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/two_simple_products_with_tier_price.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/two_simple_products_with_tier_price.php new file mode 100644 index 0000000000000..6272cab188db6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/two_simple_products_with_tier_price.php @@ -0,0 +1,74 @@ +create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); + +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(100) + ->setWeight(1) + ->setTierPrice([0 => ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 5, 'price' => 95]]) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCanSaveCustomOptions(true) + ->setStockData( + [ + 'qty' => 10, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); +$productRepository->save($product); +$product->unsetData()->setOrigData(); + +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Second Simple Product') + ->setSku('second_simple') + ->setPrice(200) + ->setWeight(1) + ->setTierPrice( + [ + 0 => [ + 'website_id' => 0, + 'cust_group' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'price_qty' => 10, + 'price' => 3, + 'percentage_value' => 3, + ], + ] + ) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCanSaveCustomOptions(true) + ->setStockData( + [ + 'qty' => 10, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/two_simple_products_with_tier_price_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/two_simple_products_with_tier_price_rollback.php new file mode 100644 index 0000000000000..d5c3caf35536a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/two_simple_products_with_tier_price_rollback.php @@ -0,0 +1,18 @@ +get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple', 'second_simple'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 183ba86ca7572..4753d947e9d3c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -8,6 +8,10 @@ namespace Magento\CatalogImportExport\Model\Export; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Framework\App\Config\ReinitableConfigInterface; + /** * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php * @magentoAppIsolation enabled @@ -32,6 +36,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ protected $fileSystem; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * Stock item attributes which must be exported * @@ -69,6 +78,7 @@ protected function setUp() $this->model = $this->objectManager->create( \Magento\CatalogImportExport\Model\Export\Product::class ); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); } /** @@ -404,6 +414,33 @@ public function testExportWithCustomOptions(): void self::assertSame($expectedData, $customOptionData); } + /** + * Check that no duplicate entities when multiple custom options used + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_options.php + */ + public function testExportWithMultipleOptions() + { + $expectedCount = 1; + $resultsFilename = 'export_results.csv'; + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + + $varDirectory = $this->objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR); + $varDirectory->writeFile($resultsFilename, $exportData); + /** @var \Magento\Framework\File\Csv $csv */ + $csv = $this->objectManager->get(\Magento\Framework\File\Csv::class); + $data = $csv->getData($varDirectory->getAbsolutePath($resultsFilename)); + $actualCount = count($data) - 1; + + $this->assertSame($expectedCount, $actualCount); + } + /** * @param string $exportedCustomOption * @return array @@ -432,4 +469,70 @@ function ($input) { return $optionItems; } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ + public function testExportProductWithTwoWebsites() + { + $globalStoreCode = 'admin'; + $secondStoreCode = 'fixture_second_store'; + + $expectedData = [ + $globalStoreCode => 10.0, + $secondStoreCode => 9.99 + ]; + + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $reinitiableConfig = $this->objectManager->get(ReinitableConfigInterface::class); + $observer = $this->objectManager->get(\Magento\Framework\Event\Observer::class); + $switchPriceScope = $this->objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class); + /** @var \Magento\Catalog\Model\Product\Action $productAction */ + $productAction = $this->objectManager->create(\Magento\Catalog\Model\Product\Action::class); + /** @var \Magento\Framework\File\Csv $csv */ + $csv = $this->objectManager->get(\Magento\Framework\File\Csv::class); + /** @var $varDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ + $varDirectory = $this->objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR); + $secondStore = $store->load($secondStoreCode); + + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + + $reinitiableConfig->setValue('catalog/price/scope', \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE); + $switchPriceScope->execute($observer); + + $product = $this->productRepository->get('simple'); + $productId = $product->getId(); + $productAction->updateWebsites([$productId], [$secondStore->getWebsiteId()], 'add'); + $product->setStoreId($secondStore->getId()); + $product->setPrice('9.99'); + $product->getResource()->save($product); + + $exportData = $this->model->export(); + + $varDirectory->writeFile('test_product_with_two_websites.csv', $exportData); + $data = $csv->getData($varDirectory->getAbsolutePath('test_product_with_two_websites.csv')); + + $columnNumber = array_search('price', $data[0]); + $this->assertNotFalse($columnNumber); + + $pricesData = [ + $globalStoreCode => (float)$data[1][$columnNumber], + $secondStoreCode => (float)$data[2][$columnNumber], + ]; + + self::assertSame($expectedData, $pricesData); + + $reinitiableConfig->setValue('catalog/price/scope', \Magento\Store\Model\Store::PRICE_SCOPE_GLOBAL); + $switchPriceScope->execute($observer); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 1b33cd695d06e..3a039217d61fc 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); /** * Test class for \Magento\CatalogImportExport\Model\Import\Product * * The "CouplingBetweenObjects" warning is caused by tremendous complexity of the original class - * */ namespace Magento\CatalogImportExport\Model\Import; @@ -16,18 +16,22 @@ use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Registry; use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\Source\Csv; use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; use Psr\Log\LoggerInterface; -use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; /** * Class ProductTest @@ -98,7 +102,7 @@ protected function tearDown() try { $product = $productRepository->get($productSku, false, null, true); $productRepository->delete($product); - // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { // nothing to delete } @@ -311,7 +315,6 @@ public function testStockState() * @throws \Magento\Framework\Exception\LocalizedException * @throws \Magento\Framework\Exception\NoSuchEntityException * @magentoAppIsolation enabled - * * @return void */ @@ -846,6 +849,37 @@ public function testSaveMediaImage() $this->assertEquals('Additional Image Label Two', $additionalImageTwoItem->getLabel()); } + /** + * Tests that "hide_from_product_page" attribute is hidden after importing product images. + * + * @magentoDataFixture mediaImportImageFixture + * @magentoAppIsolation enabled + */ + public function testSaveHiddenImages() + { + $this->importDataForMediaTest('import_media_hidden_images.csv'); + $product = $this->getProductBySku('simple_new'); + $images = $product->getMediaGalleryEntries(); + + $hiddenImages = array_filter( + $images, + static function (DataObject $image) { + return (int)$image->getDisabled() === 1; + } + ); + + $this->assertCount(3, $hiddenImages); + + $imageItem = array_shift($hiddenImages); + $this->assertEquals('/m/a/magento_image.jpg', $imageItem->getFile()); + + $imageItem = array_shift($hiddenImages); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $imageItem->getFile()); + + $imageItem = array_shift($hiddenImages); + $this->assertEquals('/m/a/magento_additional_image_two.jpg', $imageItem->getFile()); + } + /** * Test that new images should be added after the existing ones. * @@ -917,15 +951,15 @@ public function testSaveMediaImageError() */ public static function mediaImportImageFixture() { - /** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ - $mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + /** @var \Magento\Framework\Filesystem\Directory\Write $varDirectory */ + $varDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class )->getDirectoryWrite( - DirectoryList::MEDIA + DirectoryList::VAR_DIR ); - $mediaDirectory->create('import'); - $dirPath = $mediaDirectory->getAbsolutePath('import'); + $varDirectory->create('import' . DIRECTORY_SEPARATOR . 'images'); + $dirPath = $varDirectory->getAbsolutePath('import' . DIRECTORY_SEPARATOR . 'images'); $items = [ [ @@ -968,13 +1002,15 @@ public static function mediaImportImageFixture() */ public static function mediaImportImageFixtureRollback() { - /** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ - $mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $fileSystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class - )->getDirectoryWrite( - DirectoryList::MEDIA ); - $mediaDirectory->delete('import'); + /** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ + $mediaDirectory = $fileSystem->getDirectoryWrite(DirectoryList::MEDIA); + + /** @var \Magento\Framework\Filesystem\Directory\Write $varDirectory */ + $varDirectory = $fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $varDirectory->delete('import'); $mediaDirectory->delete('catalog'); } @@ -983,13 +1019,13 @@ public static function mediaImportImageFixtureRollback() */ public static function mediaImportImageFixtureError() { - /** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ - $mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + /** @var \Magento\Framework\Filesystem\Directory\Write $varDirectory */ + $varDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class )->getDirectoryWrite( - DirectoryList::MEDIA + DirectoryList::VAR_DIR ); - $dirPath = $mediaDirectory->getAbsolutePath('import'); + $dirPath = $varDirectory->getAbsolutePath('import' . DIRECTORY_SEPARATOR . 'images'); $items = [ [ 'source' => __DIR__ . '/_files/magento_additional_image_error.jpg', @@ -1540,6 +1576,49 @@ public function testValidateUrlKeysMultipleStores() $this->assertTrue($errors->getErrorsCount() == 0); } + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_product_links_data.php + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testProductLinksWithEmptyValue() + { + // import data from CSV file + $pathToFile = __DIR__ . '/_files/products_to_import_with_product_links_with_empty_value.csv'; + $filesystem = BootstrapHelper::getObjectManager()->create(Filesystem::class); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + $errors = $this->_model->setSource( + $source + )->setParameters( + [ + 'behavior' => Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ] + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $objectManager = BootstrapHelper::getObjectManager(); + $resource = $objectManager->get(ProductResource::class); + $productId = $resource->getIdBySku('simple'); + /** @var \Magento\Catalog\Model\Product $product */ + $product = BootstrapHelper::getObjectManager()->create(Product::class); + $product->load($productId); + + $this->assertEmpty($product->getCrossSellProducts()); + $this->assertEmpty($product->getUpSellProducts()); + } + /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled @@ -1744,7 +1823,11 @@ public function testUpdateUrlRewritesOnImport() /** @var \Magento\Catalog\Model\Product $product */ $product = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); - + $listOfProductUrlKeys = [ + sprintf('%s.html', $product->getUrlKey()), + sprintf('men/tops/%s.html', $product->getUrlKey()), + sprintf('men/%s.html', $product->getUrlKey()) + ]; $repUrlRewriteCol = $this->objectManager->create( UrlRewriteCollection::class ); @@ -1754,18 +1837,15 @@ public function testUpdateUrlRewritesOnImport() ->addFieldToFilter('entity_id', ['eq'=> $product->getEntityId()]) ->addFieldToFilter('entity_type', ['eq'=> 'product']) ->load(); + $listOfUrlRewriteIds = $collUrlRewrite->getAllIds(); + $this->assertCount(3, $collUrlRewrite); - $this->assertCount(2, $collUrlRewrite); - - $this->assertEquals( - sprintf('%s.html', $product->getUrlKey()), - $collUrlRewrite->getFirstItem()->getRequestPath() - ); - - $this->assertContains( - sprintf('men/tops/%s.html', $product->getUrlKey()), - $collUrlRewrite->getLastItem()->getRequestPath() - ); + foreach ($listOfUrlRewriteIds as $key => $id) { + $this->assertEquals( + $listOfProductUrlKeys[$key], + $collUrlRewrite->getItemById($id)->getRequestPath() + ); + } } /** @@ -2132,8 +2212,13 @@ private function importDataForMediaTest(string $fileName, int $expectedErrors = $uploader = $this->_model->getUploader(); $mediaPath = $appParams[DirectoryList::MEDIA][DirectoryList::PATH]; - $destDir = $directory->getRelativePath($mediaPath . '/catalog/product'); - $tmpDir = $directory->getRelativePath($mediaPath . '/import'); + $varPath = $appParams[DirectoryList::VAR_DIR][DirectoryList::PATH]; + $destDir = $directory->getRelativePath( + $mediaPath . DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'product' + ); + $tmpDir = $directory->getRelativePath( + $varPath . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . 'images' + ); $directory->create($destDir); $this->assertTrue($uploader->setDestDir($destDir)); @@ -2498,6 +2583,8 @@ public function testImportImageForNonDefaultStore() */ public function testProductsWithMultipleStoresWhenMediaIsDisabled(): void { + $this->loginAdminUserWithUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -2580,4 +2667,110 @@ private function importFile(string $fileName): void $this->_model->importData(); } + + /** + * Hide product images via hide_from_product_page attribute during import CSV. + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * + * @return void + */ + public function testImagesAreHiddenAfterImport(): void + { + $expectedActiveImages = [ + [ + 'file' => '/m/a/magento_additional_image_one.jpg', + 'label' => 'Additional Image Label One', + 'disabled' => '0', + ], + [ + 'file' => '/m/a/magento_additional_image_two.jpg', + 'label' => 'Additional Image Label Two', + 'disabled' => '0', + ], + ]; + + $expectedHiddenImage = [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'disabled' => '1', + ]; + $expectedAllProductImages = array_merge( + [$expectedHiddenImage], + $expectedActiveImages + ); + + $this->importDataForMediaTest('hide_from_product_page_images.csv'); + $actualAllProductImages = []; + $product = $this->getProductBySku('simple'); + + // Check that new images were imported and existing image is disabled after import + $productMediaData = $product->getData('media_gallery'); + + $this->assertNotEmpty($productMediaData['images']); + $allProductImages = $productMediaData['images']; + $this->assertCount(3, $allProductImages, 'Images were imported incorrectly'); + + foreach ($allProductImages as $image) { + $actualAllProductImages[] = [ + 'file' => $image['file'], + 'label' => $image['label'], + 'disabled' => $image['disabled'], + ]; + } + + $this->assertEquals( + $expectedAllProductImages, + $actualAllProductImages, + 'Images are incorrect after import' + ); + + // Check that on storefront only enabled images are shown + $actualActiveImages = $product->getMediaGalleryImages(); + $this->assertSame( + $expectedActiveImages, + $actualActiveImages->toArray(['file', 'label', 'disabled'])['items'], + 'Hidden image is present on frontend after import' + ); + } + + /** + * Set the current admin session user based on a username + * + * @param string $username + */ + private function loginAdminUserWithUsername(string $username) + { + $user = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\User\Model\User::class + )->loadByUsername($username); + + /** @var $session \Magento\Backend\Model\Auth\Session */ + $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Backend\Model\Auth\Session::class + ); + $session->setUser($user); + } + + /** + * Checking product images after Add/Update import failure + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @return void + */ + public function testProductBaseImageAfterImport() + { + $this->importDataForMediaTest('import_media.csv'); + + $this->testImportWithNonExistingImage(); + + /** @var $productAfterImport \Magento\Catalog\Model\Product */ + $productAfterImport = $this->getProductBySku('simple_new'); + $this->assertNotEquals('/no/exists/image/magento_image.jpg', $productAfterImport->getData('image')); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php index f61aa7578d4a3..d1d87b6916eb6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php @@ -16,6 +16,10 @@ */ class UploaderTest extends \Magento\TestFramework\Indexer\TestCase { + /** + * Random string appended to downloaded image name + */ + const RANDOM_STRING = 'BRV8TAuR2AT88OH0'; /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -30,6 +34,10 @@ class UploaderTest extends \Magento\TestFramework\Indexer\TestCase * @var \Magento\CatalogImportExport\Model\Import\Uploader */ private $uploader; + /** + * @var \Magento\Framework\Filesystem\File\ReadInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $fileReader; /** * @inheritdoc @@ -37,7 +45,18 @@ class UploaderTest extends \Magento\TestFramework\Indexer\TestCase protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->uploader = $this->objectManager->create(\Magento\CatalogImportExport\Model\Import\Uploader::class); + $this->fileReader = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\File\ReadInterface::class); + $fileReadFactory = $this->createMock(\Magento\Framework\Filesystem\File\ReadFactory::class); + $fileReadFactory->method('create')->willReturn($this->fileReader); + $random = $this->createMock(\Magento\Framework\Math\Random::class); + $random->method('getRandomString')->willReturn(self::RANDOM_STRING); + $this->uploader = $this->objectManager->create( + \Magento\CatalogImportExport\Model\Import\Uploader::class, + [ + 'random' => $random, + 'readFactory' => $fileReadFactory + ] + ); $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); @@ -60,19 +79,60 @@ protected function setUp() parent::setUp(); } + /** + * Tests move with external url + * + * @magentoAppIsolation enabled + * @return void + */ + public function testMoveWithExternalURL(): void + { + $fileName = 'http://magento.com/static/images/random_image.jpg'; + $this->fileReader->method('readAll')->willReturn(file_get_contents($this->getTestImagePath())); + $this->uploader->move($fileName); + $destFilePath = $this->uploader->getTmpDir() . '/' . 'random_image_' . self::RANDOM_STRING . '.jpg'; + $this->assertTrue($this->directory->isExist($destFilePath)); + } + /** * @magentoAppIsolation enabled * @return void */ public function testMoveWithValidFile(): void { - $fileName = 'magento_additional_image_one.jpg'; + $testImagePath = $this->getTestImagePath(); + $fileName = basename($testImagePath); $filePath = $this->directory->getAbsolutePath($this->uploader->getTmpDir() . '/' . $fileName); - copy(__DIR__ . '/_files/' . $fileName, $filePath); + //phpcs:ignore + copy($testImagePath, $filePath); $this->uploader->move($fileName); $this->assertTrue($this->directory->isExist($this->uploader->getTmpDir() . '/' . $fileName)); } + /** + * Check validation against temporary directory. + * + * @magentoAppIsolation enabled + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testMoveWithFileOutsideTemp(): void + { + $tmpDir = $this->uploader->getTmpDir(); + $newTmpDir = $tmpDir . '/test1'; + if (!$this->directory->create($newTmpDir)) { + throw new \RuntimeException('Failed to create temp dir'); + } + $this->uploader->setTmpDir($newTmpDir); + $testImagePath = $this->getTestImagePath(); + $fileName = basename($testImagePath); + $filePath = $this->directory->getAbsolutePath($tmpDir . '/' . $fileName); + //phpcs:ignore + copy($testImagePath, $filePath); + $this->uploader->move('../' . $fileName); + $this->assertTrue($this->directory->isExist($tmpDir . '/' . $fileName)); + } + /** * @magentoAppIsolation enabled * @return void @@ -83,8 +143,19 @@ public function testMoveWithInvalidFile(): void { $fileName = 'media_import_image.php'; $filePath = $this->directory->getAbsolutePath($this->uploader->getTmpDir() . '/' . $fileName); + //phpcs:ignore copy(__DIR__ . '/_files/' . $fileName, $filePath); $this->uploader->move($fileName); $this->assertFalse($this->directory->isExist($this->uploader->getTmpDir() . '/' . $fileName)); } + + /** + * Get the full path to the test image + * + * @return string + */ + private function getTestImagePath(): string + { + return __DIR__ . '/_files/magento_additional_image_one.jpg'; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/hide_from_product_page_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/hide_from_product_page_images.csv new file mode 100644 index 0000000000000..5902723ae5024 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/hide_from_product_page_images.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values +simple,,Default,simple,,base,Simple Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,,meta title,meta keyword,meta description,magento_additional_image_one.jpg,Additional Image Label One,magento_additional_image_one.jpg,Small Additional Image Label One,magento_additional_image_one.jpg,Thumbnail Label,magento_additional_image_one.jpg,Additional Image Label One,,,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,"magento_additional_image_one.jpg,magento_additional_image_two.jpg","Additional Image Label One,Additional Image Label Two",/m/a/magento_image.jpg,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_hidden_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_hidden_images.csv new file mode 100644 index 0000000000000..1c1bebee57578 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_hidden_images.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product,magento_image.jpg,Image Label,magento_small_image.jpg,Small Image Label,magento_thumbnail.jpg,Thumbnail Label,magento_image.jpg,Image Label,10/20/2015 7:05,10/20/2015 7:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,"magento_additional_image_one.jpg, magento_additional_image_two.jpg","Additional Image Label One,Additional Image Label Two","magento_image.jpg,magento_thumbnail.jpg,magento_additional_image_two.jpg",,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php index 04b3092c8fa8a..0ee59aedd8979 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php @@ -4,16 +4,23 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ -$mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( +/** @var \Magento\Framework\Filesystem $fileSystem */ +$fileSystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class -)->getDirectoryWrite( +); +/** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ +$mediaDirectory = $fileSystem->getDirectoryWrite( \Magento\Framework\App\Filesystem\DirectoryList::MEDIA ); +/** @var \Magento\Framework\Filesystem\Directory\Write $varDirectory */ +$varDirectory = $fileSystem->getDirectoryWrite( + \Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR +); $path = 'catalog' . DIRECTORY_SEPARATOR . 'product'; +$varImagesPath = 'import' . DIRECTORY_SEPARATOR . 'images'; // Is required for using importDataForMediaTest method. -$mediaDirectory->create('import'); +$varDirectory->create($varImagesPath); $mediaDirectory->create($path); $dirPath = $mediaDirectory->getAbsolutePath($path); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images_rollback.php index 5c1db3ca045a6..a984cbf2e3529 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images_rollback.php @@ -4,11 +4,17 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ -$mediaDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( +/** @var \Magento\Framework\Filesystem $fileSystem */ +$fileSystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class -)->getDirectoryWrite( +); +/** @var \Magento\Framework\Filesystem\Directory\Write $mediaDirectory */ +$mediaDirectory = $fileSystem->getDirectoryWrite( \Magento\Framework\App\Filesystem\DirectoryList::MEDIA ); -$mediaDirectory->delete('import'); +/** @var \Magento\Framework\Filesystem\Directory\Write $varDirectory */ +$varDirectory = $fileSystem->getDirectoryWrite( + \Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR +); +$varDirectory->delete('import'); $mediaDirectory->delete('catalog'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_product_links_with_empty_value.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_product_links_with_empty_value.csv new file mode 100644 index 0000000000000..fbbf6e2fb33f2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_product_links_with_empty_value.csv @@ -0,0 +1,2 @@ +sku,crosssell_skus,crosssell_position,upsell_skus,upsell_position +simple,__EMPTY__VALUE__,__EMPTY__VALUE__,__EMPTY__VALUE__,__EMPTY__VALUE__ diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Block/Adminhtml/Form/Field/CustomergroupTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Block/Adminhtml/Form/Field/CustomergroupTest.php index 527828a92dea8..b7be72e9ff827 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Block/Adminhtml/Form/Field/CustomergroupTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Block/Adminhtml/Form/Field/CustomergroupTest.php @@ -22,11 +22,12 @@ protected function setUp() public function testToHtml() { - $this->_block->setClass('customer_group_select'); + $this->_block->setClass('customer_group_select admin__control-select'); $this->_block->setId('123'); $this->_block->setTitle('Customer Group'); $this->_block->setInputName('groups[item_options]'); - $expectedResult = ''; diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Indexer/Stock/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Indexer/Stock/Action/FullTest.php index 00c13619ff2c1..6bf1f5fbf0be2 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Indexer/Stock/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Indexer/Stock/Action/FullTest.php @@ -5,42 +5,24 @@ */ namespace Magento\CatalogInventory\Model\Indexer\Stock\Action; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\ObjectManagerInterface; -use Magento\CatalogInventory\Model\Indexer\Stock\Processor; -use Magento\Catalog\Model\CategoryFactory; -use Magento\Catalog\Block\Product\ListProduct; -use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; -use Magento\Catalog\Model\Product; -use PHPUnit\Framework\TestCase; - /** * Full reindex Test */ -class FullTest extends TestCase +class FullTest extends \PHPUnit\Framework\TestCase { /** - * @var ObjectManagerInterface - */ - private $objectManager; - - /** - * @var Processor + * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor */ protected $_processor; - /** - * @inheritdoc - */ protected function setUp() { - $this->objectManager = Bootstrap::getObjectManager(); - $this->_processor = $this->objectManager->get(Processor::class); + $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class + ); } /** - * Reindex all - * * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @magentoDataFixture Magento/Catalog/_files/product_simple.php @@ -49,9 +31,13 @@ public function testReindexAll() { $this->_processor->reindexAll(); - $categoryFactory = $this->objectManager->get(CategoryFactory::class); - /** @var ListProduct $listProduct */ - $listProduct = $this->objectManager->get(ListProduct::class); + $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Catalog\Model\CategoryFactory::class + ); + /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ + $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Catalog\Block\Product\ListProduct::class + ); $category = $categoryFactory->create()->load(2); $layer = $listProduct->getLayer(); @@ -75,37 +61,4 @@ public function testReindexAll() $this->assertEquals(100, $product->getQty()); } } - - /** - * Reindex with disabled product - * - * @return void - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php - */ - public function testReindexAllWithDisabledProduct(): void - { - $productCollectionFactory = $this->objectManager->get(CollectionFactory::class); - $productCollection = $productCollectionFactory - ->create() - ->addAttributeToSelect('*') - ->addAttributeToFilter('sku', ['eq' => 'simple3']) - ->addAttributeToSort('created_at', 'DESC') - ->joinField( - 'stock_status', - 'cataloginventory_stock_status', - 'stock_status', - 'product_id=entity_id', - '{{table}}.stock_id=1', - 'left' - )->load(); - - $this->assertCount(1, $productCollection); - - /** @var Product $product */ - foreach ($productCollection as $product) { - $this->assertEquals(1, $product->getData('stock_status')); - } - } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php index 4f8a279a59165..24ad6af1fea51 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php @@ -8,6 +8,7 @@ /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php */ class ResultTest extends \Magento\TestFramework\TestCase\AbstractController { @@ -31,6 +32,9 @@ public function testIndexActionTranslation() $this->assertContains('Den gesamten Shop durchsuchen...', $responseBody); } + /** + * @magentoDbIsolation disabled + */ public function testIndexActionXSSQueryVerification() { $escaper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php index 4d2a90f8f44f9..d503c9678dfd6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php @@ -7,37 +7,71 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository as ProductRepository; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Api\Search\Document as SearchDocument; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Search\AdapterInterface as AdapterInterface; +use Magento\Framework\Search\Request\Builder as SearchRequestBuilder; +use Magento\Framework\Search\Request\Config as SearchRequestConfig; +use Magento\Search\Model\AdapterFactory as AdapterFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + class DataProviderTest extends \PHPUnit\Framework\TestCase { + /** + * @inheritdoc + */ + public static function setUpBeforeClass() + { + /* + * Due to insufficient search engine isolation for Elasticsearch, this class must explicitly perform + * a fulltext reindex prior to running its tests. + * + * This should be removed upon completing MC-19455. + */ + $indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + $fulltextIndexer = $indexRegistry->get(Fulltext::INDEXER_ID); + $fulltextIndexer->reindexAll(); + } + /** * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search.php * @magentoDbIsolation disabled */ public function testSearchProductByAttribute() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ObjectManager $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + + /** @var SearchRequestConfig $config */ + $config = $objectManager->create(SearchRequestConfig::class); - $config = $objectManager->create(\Magento\Framework\Search\Request\Config::class); - /** @var \Magento\Framework\Search\Request\Builder $requestBuilder */ + /** @var SearchRequestBuilder $requestBuilder */ $requestBuilder = $objectManager->create( - \Magento\Framework\Search\Request\Builder::class, + SearchRequestBuilder::class, ['config' => $config] ); + $requestBuilder->bind('search_term', 'VALUE1'); $requestBuilder->setRequestName('quick_search_container'); $queryRequest = $requestBuilder->create(); - /** @var \Magento\Framework\Search\Adapter\Mysql\Adapter $adapter */ - $adapter = $objectManager->create(\Magento\Framework\Search\Adapter\Mysql\Adapter::class); + + /** @var AdapterInterface $adapter */ + $adapterFactory = $objectManager->create(AdapterFactory::class); + $adapter = $adapterFactory->create(); $queryResponse = $adapter->query($queryRequest); $actualIds = []; + foreach ($queryResponse as $document) { - /** @var \Magento\Framework\Api\Search\Document $document */ + /** @var SearchDocument $document */ $actualIds[] = $document->getId(); } - /** @var \Magento\Catalog\Model\Product $product */ - $product = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); + /** @var Product $product */ + $product = $objectManager->create(ProductRepository::class)->get('simple'); $this->assertContains($product->getId(), $actualIds, 'Product not found by searchable attribute.'); } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 137a3845b1efa..a5c18f0fcee6c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -16,38 +16,36 @@ /** * Class for testing fulltext index rebuild + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FullTest extends \PHPUnit\Framework\TestCase { - /** - * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full - */ - protected $actionFull; - - /** - * @inheritdoc - */ - protected function setUp() - { - $this->actionFull = Bootstrap::getObjectManager()->create( - \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class - ); - } - /** * Testing fulltext index rebuild * * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php + * @magentoConfigFixture default/catalog/search/engine mysql */ public function testGetIndexData() { + $engineProvider = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\ResourceModel\EngineProvider::class + ); + $dataProvider = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::class, + ['engineProvider' => $engineProvider] + ); + $actionFull = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class, + ['dataProvider' => $dataProvider] + ); /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); $allowedStatuses = Bootstrap::getObjectManager()->get(Status::class)->getVisibleStatusIds(); $allowedVisibility = Bootstrap::getObjectManager()->get(Engine::class)->getAllowedVisibility(); - $result = iterator_to_array($this->actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); + $result = iterator_to_array($actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); $this->assertNotEmpty($result); $productsIds = array_keys($result); @@ -82,37 +80,45 @@ private function getExpectedIndexData() $taxClassId = $attributeRepository ->get(\Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID) ->getAttributeId(); + $urlKeyId = $attributeRepository + ->get(\Magento\Catalog\Api\Data\ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY) + ->getAttributeId(); return [ 'configurable' => [ $skuId => 'configurable', $configurableId => 'Option 2', $nameId => 'Configurable Product | Configurable OptionOption 2', $taxClassId => 'Taxable Goods | Taxable Goods', - $statusId => 'Enabled | Enabled' + $statusId => 'Enabled | Enabled', + $urlKeyId => 'configurable-product | configurable-optionoption-2' ], 'index_enabled' => [ $skuId => 'index_enabled', $nameId => 'index enabled', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-enabled' ], 'index_visible_search' => [ $skuId => 'index_visible_search', $nameId => 'index visible search', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-search' ], 'index_visible_category' => [ $skuId => 'index_visible_category', $nameId => 'index visible category', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-category' ], 'index_visible_both' => [ $skuId => 'index_visible_both', $nameId => 'index visible both', $taxClassId => 'Taxable Goods', - $statusId => 'Enabled' + $statusId => 'Enabled', + $urlKeyId => 'index-visible-both' ] ]; } @@ -124,6 +130,9 @@ private function getExpectedIndexData() */ public function testRebuildStoreIndexConfigurable() { + $actionFull = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class + ); $storeId = 1; $simpleProductId = $this->getIdBySku('simple_10'); @@ -133,8 +142,8 @@ public function testRebuildStoreIndexConfigurable() $simpleProductId, $configProductId ]; - $storeIndexDataSimple = $this->actionFull->rebuildStoreIndex($storeId, [$simpleProductId]); - $storeIndexDataExpected = $this->actionFull->rebuildStoreIndex($storeId, $expected); + $storeIndexDataSimple = $actionFull->rebuildStoreIndex($storeId, [$simpleProductId]); + $storeIndexDataExpected = $actionFull->rebuildStoreIndex($storeId, $expected); $this->assertEquals($storeIndexDataSimple, $storeIndexDataExpected); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php index 7c72d18b97118..b0ae104cae393 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php @@ -9,7 +9,6 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Visibility; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; use Magento\TestFramework\Helper\Bootstrap; /** @@ -76,10 +75,6 @@ protected function setUp() ); $this->indexer->load('catalogsearch_fulltext'); - $this->engine = Bootstrap::getObjectManager()->get( - \Magento\CatalogSearch\Model\ResourceModel\Engine::class - ); - $this->queryFactory = Bootstrap::getObjectManager()->get( \Magento\Search\Model\QueryFactory::class ); @@ -223,12 +218,8 @@ protected function search(string $text, $visibilityFilter = null): array $query->setQueryText($text); $query->saveIncrementalPopularity(); $products = []; - $collection = Bootstrap::getObjectManager()->create( - Collection::class, - [ - 'searchRequestName' => 'quick_search_container' - ] - ); + $searchLayer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Layer\Search::class); + $collection = $searchLayer->getProductCollection(); $collection->addSearchFilter($text); if (null !== $visibilityFilter) { diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php index f0c8402c51879..b75a984178f24 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php @@ -48,6 +48,46 @@ protected function setUp() ->create(\Magento\CatalogSearch\Model\Layer\Filter\Decimal::class, ['layer' => $layer]); $this->_model->setAttributeModel($attribute); } + + /** + * Test the filter label is correct + */ + public function testApplyFilterLabel() + { + /** @var $objectManager \Magento\TestFramework\ObjectManager */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var $request \Magento\TestFramework\Request */ + $request = $objectManager->get(\Magento\TestFramework\Request::class); + $request->setParam('weight', '10-20'); + $this->_model->apply($request); + + $filters = $this->_model->getLayer()->getState()->getFilters(); + $this->assertArrayHasKey(0, $filters); + $this->assertEquals( + '$10.00 - $19.99', + (string)$filters[0]->getLabel() + ); + } + + /** + * Test the filter label is correct when there is empty To value + */ + public function testApplyFilterLabelWithEmptyToValue() + { + /** @var $objectManager \Magento\TestFramework\ObjectManager */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var $request \Magento\TestFramework\Request */ + $request = $objectManager->get(\Magento\TestFramework\Request::class); + $request->setParam('weight', '10-'); + $this->_model->apply($request); + + $filters = $this->_model->getLayer()->getState()->getFilters(); + $this->assertArrayHasKey(0, $filters); + $this->assertEquals( + '$10.00 and above', + (string)$filters[0]->getLabel() + ); + } public function testApplyNothing() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php index 451553113af2c..a7944566eb8e0 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Layer\Filter; use Magento\TestFramework\Helper\Bootstrap; @@ -35,10 +37,16 @@ protected function setUp() $category->load(4); $layer = $this->objectManager->get(\Magento\Catalog\Model\Layer\Category::class); $layer->setCurrentCategory($category); + /** @var $attribute \Magento\Catalog\Model\Entity\Attribute */ + $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Entity\Attribute::class + ); + $attribute->loadByCode('catalog_product', 'price'); $this->_model = $this->objectManager->create( \Magento\CatalogSearch\Model\Layer\Filter\Price::class, ['layer' => $layer] ); + $this->_model->setAttributeModel($attribute); } public function testApplyNothing() diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php index 5dcff3f92a9f9..3eea0aa117452 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php @@ -5,6 +5,10 @@ */ namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\TestFramework\Helper\Bootstrap; + /** * Test class for \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection. * @magentoDbIsolation disabled @@ -18,8 +22,12 @@ class CollectionTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->advancedCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class); + $advanced = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\CatalogSearch\Model\Search\ItemCollectionProvider::class); + $this->advancedCollection = $advanced->getCollection(); + $indexerRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(IndexerRegistry::class); + $indexerRegistry->get(Fulltext::INDEXER_ID)->reindexAll(); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php index 93df194080b69..8863834078214 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php @@ -14,6 +14,9 @@ class CollectionTest extends \PHPUnit\Framework\TestCase /** * @dataProvider filtersDataProviderSearch * @magentoDataFixture Magento/Framework/Search/_files/products.php + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + * @magentoConfigFixture default/catalog/search/engine mysql + * @magentoAppIsolation enabled */ public function testLoadWithFilterSearch($request, $filters, $expectedCount) { @@ -26,6 +29,48 @@ public function testLoadWithFilterSearch($request, $filters, $expectedCount) foreach ($filters as $field => $value) { $fulltextCollection->addFieldToFilter($field, $value); } + if ($request == 'quick_search_container' && isset($filters['search_term'])) { + $fulltextCollection->addSearchFilter($filters['search_term']); + } + $fulltextCollection->loadWithFilter(); + $items = $fulltextCollection->getItems(); + $this->assertCount($expectedCount, $items); + } + + /** + * @dataProvider filtersDataProviderQuickSearch + * @magentoDataFixture Magento/Framework/Search/_files/products.php + */ + public function testLoadWithFilterQuickSearch($filters, $expectedCount) + { + $objManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $searchLayer = $objManager->create(\Magento\Catalog\Model\Layer\Search::class); + /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $fulltextCollection */ + $fulltextCollection = $searchLayer->getProductCollection(); + foreach ($filters as $field => $value) { + $fulltextCollection->addFieldToFilter($field, $value); + } + if (isset($filters['search_term'])) { + $fulltextCollection->addSearchFilter($filters['search_term']); + } + $fulltextCollection->loadWithFilter(); + $items = $fulltextCollection->getItems(); + $this->assertCount($expectedCount, $items); + } + + /** + * @dataProvider filtersDataProviderCatalogView + * @magentoDataFixture Magento/Framework/Search/_files/products.php + */ + public function testLoadWithFilterCatalogView($filters, $expectedCount) + { + $objManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $searchLayer = $objManager->create(\Magento\Catalog\Model\Layer\Category::class); + /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $fulltextCollection */ + $fulltextCollection = $searchLayer->getProductCollection(); + foreach ($filters as $field => $value) { + $fulltextCollection->addFieldToFilter($field, $value); + } $fulltextCollection->loadWithFilter(); $items = $fulltextCollection->getItems(); $this->assertCount($expectedCount, $items); @@ -42,13 +87,11 @@ public function testSearchResultsAreTheSameForSameRequests() $objManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); foreach (range(1, $howManySearchRequests) as $i) { + $searchLayer = $objManager->create(\Magento\Catalog\Model\Layer\Search::class); /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $fulltextCollection */ - $fulltextCollection = $objManager->create( - \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection::class, - ['searchRequestName' => 'quick_search_container'] - ); + $fulltextCollection = $searchLayer->getProductCollection(); - $fulltextCollection->addFieldToFilter('search_term', 'shorts'); + $fulltextCollection->addSearchFilter('shorts'); $fulltextCollection->setOrder('relevance'); $fulltextCollection->load(); $items = $fulltextCollection->getItems(); @@ -81,4 +124,22 @@ public function filtersDataProviderSearch() ['catalog_view_container', [], 0], ]; } + + public function filtersDataProviderQuickSearch() + { + return [ + [['search_term' => ' shorts'], 2], + [['search_term' => 'nonexistent'], 0], + ]; + } + + public function filtersDataProviderCatalogView() + { + return [ + [['category_ids' => 2], 5], + [['category_ids' => 100001], 0], + [['category_ids' => []], 5], + [[], 5], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php index 2ed7c1a45360d..0e5987f8326a5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/indexer_fulltext.php @@ -14,6 +14,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Apple') ->setSku('fulltext-1') + ->setUrlKey('fulltext-1') ->setPrice(10) ->setMetaTitle('first meta title') ->setMetaKeyword('first meta keyword') @@ -30,6 +31,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Banana') ->setSku('fulltext-2') + ->setUrlKey('fulltext-2') ->setPrice(20) ->setMetaTitle('second meta title') ->setMetaKeyword('second meta keyword') @@ -46,6 +48,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Orange') ->setSku('fulltext-3') + ->setUrlKey('fulltext-3') ->setPrice(20) ->setMetaTitle('third meta title') ->setMetaKeyword('third meta keyword') @@ -62,6 +65,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Papaya') ->setSku('fulltext-4') + ->setUrlKey('fulltext-4') ->setPrice(20) ->setMetaTitle('fourth meta title') ->setMetaKeyword('fourth meta keyword') @@ -78,6 +82,7 @@ ->setWebsiteIds([1]) ->setName('Simple Product Cherry') ->setSku('fulltext-5') + ->setUrlKey('fulltext-5') ->setPrice(20) ->setMetaTitle('fifth meta title') ->setMetaKeyword('fifth meta keyword') diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/AbstractUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/AbstractUrlRewriteTest.php new file mode 100644 index 0000000000000..251a79f46e38c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/AbstractUrlRewriteTest.php @@ -0,0 +1,133 @@ +objectManager = Bootstrap::getObjectManager(); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->urlRewriteCollectionFactory = $this->objectManager->get(UrlRewriteCollectionFactory::class); + } + + /** + * Retrieve all rewrite ids + * + * @return array + */ + protected function getAllRewriteIds(): array + { + $urlRewriteCollection = $this->urlRewriteCollectionFactory->create(); + + return $urlRewriteCollection->getAllIds(); + } + + /** + * Check that actual data contains of expected values + * + * @param UrlRewriteCollection $collection + * @param array $expectedData + * @return void + */ + protected function assertRewrites(UrlRewriteCollection $collection, array $expectedData): void + { + $collectionItems = $collection->toArray()['items']; + $this->assertTrue(count($collectionItems) === count($expectedData)); + foreach ($expectedData as $expectedItem) { + $found = false; + foreach ($collectionItems as $item) { + $found = array_intersect_assoc($item, $expectedItem) == $expectedItem; + if ($found) { + break; + } + } + $this->assertTrue($found, 'The actual data does not contains of expected values'); + } + } + + /** + * Get category url rewrites collection + * + * @param string|array $entityId + * @return UrlRewriteCollection + */ + protected function getEntityRewriteCollection($entityId): UrlRewriteCollection + { + $condition = is_array($entityId) ? ['in' => $entityId] : $entityId; + $entityRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $entityRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_ID, $condition) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => $this->getEntityType()]); + + return $entityRewriteCollection; + } + + /** + * Prepare expected data + * + * @param array $expectedData + * @param int|null $id + * @return array + */ + protected function prepareData(array $expectedData, ?int $id = null): array + { + $newData = []; + foreach ($expectedData as $key => $expectedItem) { + $newData[$key] = str_replace(['%suffix%', '%id%'], [$this->getUrlSuffix(), $id], $expectedItem); + } + + return $newData; + } + + /** + * Get entity type + * + * @return string + */ + abstract protected function getEntityType(): string; + + /** + * Get config value for url suffix + * + * @return string + */ + abstract protected function getUrlSuffix(): string; +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php index 1c2ee602c3bd4..b6fa2fac2ca80 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGeneratorTest.php @@ -98,12 +98,6 @@ public function testGenerateUrlRewritesWithoutSaveHistory() 'catalog/product/view/id/' . $productForTest . '/category/4', 1, 0 - ], - [ - '/simple-product-two.html', - 'catalog/product/view/id/' . $productForTest . '/category/2', - 1, - 0 ] ]; @@ -187,12 +181,6 @@ public function testGenerateUrlRewritesWithSaveHistory() 1, 0 ], - [ - '/simple-product-two.html', - 'catalog/product/view/id/' . $productForTest . '/category/2', - 1, - 0 - ], [ 'category-1/simple-product-two.html', 'new-url/simple-product-two.html', @@ -329,6 +317,8 @@ public function testGenerateUrlRewritesWithoutGenerateProductRewrites() * @magentoAppIsolation enabled * * @return void + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ public function testRemoveCatalogUrlRewrites() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php new file mode 100644 index 0000000000000..1431148c5f868 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php @@ -0,0 +1,409 @@ +categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); + $this->categoryResource = $this->objectManager->get(CategoryResource::class); + $this->categoryLinkManagement = $this->objectManager->create(CategoryLinkManagementInterface::class); + $this->categoryFactory = $this->objectManager->get(CategoryFactory::class); + $this->suffix = $this->config->getValue( + CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_position.php + * @dataProvider categoryProvider + * @param array $data + * @return void + */ + public function testUrlRewriteOnCategorySave(array $data): void + { + $categoryModel = $this->saveCategory($data['data']); + $this->assertNotNull($categoryModel->getId(), 'The category was not created'); + $urlRewriteCollection = $this->getEntityRewriteCollection($categoryModel->getId()); + $this->assertRewrites( + $urlRewriteCollection, + $this->prepareData($data['expected_data'], (int)$categoryModel->getId()) + ); + } + + /** + * @return array + */ + public function categoryProvider(): array + { + return [ + 'without_url_key' => [ + [ + 'data' => [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + ], + 'expected_data' => [ + [ + 'request_path' => 'test-category%suffix%', + 'target_path' => 'catalog/category/view/id/%id%', + ], + ], + ], + ], + 'subcategory_without_url_key' => [ + [ + 'data' => [ + 'name' => 'Test Sub Category', + 'attribute_set_id' => '3', + 'parent_id' => 444, + 'path' => '1/2/444', + 'is_active' => true, + ], + 'expected_data' => [ + [ + 'request_path' => 'category-1/test-sub-category%suffix%', + 'target_path' => 'catalog/category/view/id/%id%', + ], + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @dataProvider productRewriteProvider + * @param array $data + * @return void + */ + public function testCategoryProductUrlRewrite(array $data): void + { + $category = $this->categoryRepository->get(402); + $this->categoryLinkManagement->assignProductToCategories('simple2', [$category->getId()]); + $productRewriteCollection = $this->getCategoryProductRewriteCollection( + array_keys($category->getParentCategories()) + ); + $this->assertRewrites($productRewriteCollection, $this->prepareData($data)); + } + + /** + * @return array + */ + public function productRewriteProvider(): array + { + return [ + [ + [ + [ + 'request_path' => 'category-1/category-1-1/category-1-1-1/simple-product2%suffix%', + 'target_path' => 'catalog/product/view/id/6/category/402', + ], + [ + 'request_path' => 'category-1/simple-product2%suffix%', + 'target_path' => 'catalog/product/view/id/6/category/400', + ], + [ + 'request_path' => 'category-1/category-1-1/simple-product2%suffix%', + 'target_path' => 'catalog/product/view/id/6/category/401', + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_products.php + * @magentoAppIsolation enabled + * @dataProvider existingUrlProvider + * @param array $data + * @return void + */ + public function testUrlRewriteOnCategorySaveWithExistingUrlKey(array $data): void + { + $this->expectException(UrlAlreadyExistsException::class); + $this->expectExceptionMessage((string)__('URL key for specified store already exists.')); + $this->saveCategory($data); + } + + /** + * @return array + */ + public function existingUrlProvider(): array + { + return [ + 'with_specified_existing_product_url_key' => [ + 'data' => [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + 'url_key' => 'simple-product', + ], + ], + 'with_autogenerated_existing_product_url_key' => [ + 'data' => [ + 'name' => 'Simple Product', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + ], + ], + 'with_specified_existing_category_url_key' => [ + 'data' => [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + 'url_key' => 'category-1', + ], + ], + 'with_autogenerated_existing_category_url_key' => [ + 'data' => [ + 'name' => 'Category 1', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoDataFixture Magento/Catalog/_files/catalog_category_with_slash.php + * @dataProvider categoryMoveProvider + * @param array $data + * @return void + */ + public function testUrlRewriteOnCategoryMove(array $data): void + { + $categoryId = $data['data']['id']; + $category = $this->categoryRepository->get($categoryId); + $category->move($data['data']['pid'], $data['data']['aid']); + $productRewriteCollection = $this->getCategoryProductRewriteCollection( + array_keys($category->getParentCategories()) + ); + $categoryRewriteCollection = $this->getEntityRewriteCollection($categoryId); + $this->assertRewrites($categoryRewriteCollection, $this->prepareData($data['expected_data']['category'])); + $this->assertRewrites($productRewriteCollection, $this->prepareData($data['expected_data']['product'])); + } + + /** + * @return array + */ + public function categoryMoveProvider(): array + { + return [ + 'append_category' => [ + [ + 'data' => [ + 'id' => '333', + 'pid' => '3331', + 'aid' => '0', + ], + 'expected_data' => [ + 'category' => [ + [ + 'request_path' => 'category-1.html', + 'target_path' => 'category-with-slash-symbol/category-1%suffix%', + 'redirect_type' => OptionProvider::PERMANENT, + ], + [ + 'request_path' => 'category-with-slash-symbol/category-1%suffix%', + 'target_path' => 'catalog/category/view/id/333', + ], + ], + 'product' => [ + [ + 'request_path' => 'category-with-slash-symbol/simple-product-three%suffix%', + 'target_path' => 'catalog/product/view/id/333/category/3331', + ], + [ + 'request_path' => 'category-with-slash-symbol/category-1/simple-product-three%suffix%', + 'target_path' => 'catalog/product/view/id/333/category/333', + ], + ], + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category.php + * @return void + */ + public function testUrlRewritesAfterCategoryDelete(): void + { + $categoryId = 333; + $categoryItemIds = $this->getEntityRewriteCollection($categoryId)->getAllIds(); + $this->categoryRepository->deleteByIdentifier($categoryId); + $this->assertEmpty( + array_intersect($this->getAllRewriteIds(), $categoryItemIds), + 'Not all expected category url rewrites were deleted' + ); + } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_product_ids.php + * @return void + */ + public function testUrlRewritesAfterCategoryWithProductsDelete(): void + { + $category = $this->categoryRepository->get(3); + $childIds = explode(',', $category->getAllChildren()); + $productRewriteIds = $this->getCategoryProductRewriteCollection($childIds)->getAllIds(); + $categoryItemIds = $this->getEntityRewriteCollection($childIds)->getAllIds(); + $this->categoryRepository->deleteByIdentifier($category->getId()); + $allIds = $this->getAllRewriteIds(); + $this->assertEmpty( + array_intersect($allIds, $categoryItemIds), + 'Not all expected category url rewrites were deleted' + ); + $this->assertEmpty( + array_intersect($allIds, $productRewriteIds), + 'Not all expected category-product url rewrites were deleted' + ); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * @return void + */ + public function testCategoryUrlRewritePerStoreViews(): void + { + $urlSuffix = $this->config->getValue( + CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + $urlKeySecondStore = 'url-key-for-second-store'; + $secondStoreId = $this->storeRepository->get('fixture_second_store')->getId(); + $categoryId = 333; + $category = $this->categoryRepository->get($categoryId); + $urlKeyFirstStore = $category->getUrlKey(); + $this->saveCategory( + ['store_id' => $secondStoreId, 'url_key' => $urlKeySecondStore], + $category + ); + $urlRewriteItems = $this->getEntityRewriteCollection($categoryId)->getItems(); + $this->assertTrue(count($urlRewriteItems) == 2); + foreach ($urlRewriteItems as $item) { + $item->getData('store_id') == $secondStoreId + ? $this->assertEquals($urlKeySecondStore . $urlSuffix, $item->getRequestPath()) + : $this->assertEquals($urlKeyFirstStore . $urlSuffix, $item->getRequestPath()); + } + } + + /** + * @inheritdoc + */ + protected function getUrlSuffix(): string + { + return $this->suffix; + } + + /** + * @inheritdoc + */ + protected function getEntityType(): string + { + return DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE; + } + + /** + * Save product with data using resource model directly + * + * @param array $data + * @param CategoryInterface|null $category + * @return CategoryInterface + */ + private function saveCategory(array $data, $category = null): CategoryInterface + { + $category = $category ?: $this->categoryFactory->create(); + $category->addData($data); + $this->categoryResource->save($category); + + return $category; + } + + /** + * Get products url rewrites collection referred to categories + * + * @param string|array $categoryId + * @return UrlRewriteCollection + */ + private function getCategoryProductRewriteCollection($categoryId): UrlRewriteCollection + { + $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; + $productRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $productRewriteCollection + ->join( + ['p' => $this->categoryResource->getTable(Product::TABLE_NAME)], + 'main_table.url_rewrite_id = p.url_rewrite_id', + 'category_id' + ) + ->addFieldToFilter('category_id', $condition) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataProductUrlRewriteDatabaseMap::ENTITY_TYPE]); + + return $productRewriteCollection; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php new file mode 100644 index 0000000000000..f8fe68c2e0a2d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php @@ -0,0 +1,302 @@ +productResource = $this->objectManager->create(ProductResource::class); + $this->productFactory = $this->objectManager->get(ProductFactory::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->suffix = $this->config->getValue( + ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @dataProvider productDataProvider + * @param array $data + * @return void + */ + public function testUrlRewriteOnProductSave(array $data): void + { + $product = $this->saveProduct($data['data']); + $this->assertNotNull($product->getId(), 'The product was not created'); + $productUrlRewriteCollection = $this->getEntityRewriteCollection($product->getId()); + $this->assertRewrites( + $productUrlRewriteCollection, + $this->prepareData($data['expected_data'], (int)$product->getId()) + ); + } + + /** + * @return array + */ + public function productDataProvider(): array + { + return [ + 'without_url_key' => [ + [ + 'data' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'visibility' => Visibility::VISIBILITY_BOTH, + 'attribute_set_id' => 4, + 'sku' => 'test-product', + 'name' => 'test product', + 'price' => 150, + ], + 'expected_data' => [ + [ + 'request_path' => 'test-product%suffix%', + 'target_path' => 'catalog/product/view/id/%id%', + ], + ], + ], + ], + 'with_url_key' => [ + [ + 'data' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'attribute_set_id' => 4, + 'sku' => 'test-product', + 'visibility' => Visibility::VISIBILITY_BOTH, + 'name' => 'test product', + 'price' => 150, + 'url_key' => 'test-product-url-key', + ], + 'expected_data' => [ + [ + 'request_path' => 'test-product-url-key%suffix%', + 'target_path' => 'catalog/product/view/id/%id%', + ], + ], + ], + ], + 'with_invisible_product' => [ + [ + 'data' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'attribute_set_id' => 4, + 'sku' => 'test-product', + 'visibility' => Visibility::VISIBILITY_NOT_VISIBLE, + 'name' => 'test product', + 'price' => 150, + 'url_key' => 'test-product-url-key', + ], + 'expected_data' => [], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php + * @dataProvider productEditProvider + * @param array $expectedData + * @return void + */ + public function testUrlRewriteOnProductEdit(array $expectedData): void + { + $product = $this->productRepository->get('simple'); + $data = [ + 'url_key' => 'new-url-key', + 'url_key_create_redirect' => $product->getUrlKey(), + 'save_rewrites_history' => true, + ]; + $product = $this->saveProduct($data, $product); + $productRewriteCollection = $this->getEntityRewriteCollection($product->getId()); + $this->assertRewrites( + $productRewriteCollection, + $this->prepareData($expectedData, (int)$product->getId()) + ); + } + + /** + * @return array + */ + public function productEditProvider(): array + { + return [ + [ + 'expected_data' => [ + [ + 'request_path' => 'new-url-key%suffix%', + 'target_path' => 'catalog/product/view/id/%id%', + ], + [ + 'request_path' => 'simple-product%suffix%', + 'target_path' => 'new-url-key%suffix%', + 'redirect_type' => OptionProvider::PERMANENT, + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/category_with_products.php + * @dataProvider existingUrlKeyProvider + * @param array $data + * @return void + */ + public function testUrlRewriteOnProductSaveWithExistingUrlKey(array $data): void + { + $this->expectException(UrlAlreadyExistsException::class); + $this->expectExceptionMessage((string)__('URL key for specified store already exists.')); + $this->saveProduct($data); + } + + /** + * @return array + */ + public function existingUrlKeyProvider(): array + { + return [ + [ + 'with_specified_existing_product_url_key' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'attribute_set_id' => 4, + 'sku' => 'test-simple-product', + 'name' => 'test-simple-product', + 'price' => 150, + 'url_key' => 'simple-product', + ], + 'with_autogenerated_existing_product_url_key' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'attribute_set_id' => 4, + 'sku' => 'test-simple-product', + 'name' => 'simple product', + 'price' => 150, + ], + 'with_specified_existing_category_url_key' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'attribute_set_id' => 4, + 'sku' => 'test-simple-product', + 'name' => 'test-simple-product', + 'price' => 150, + 'url_key' => 'category-1', + ], + 'with_autogenerated_existing_category_url_key' => [ + 'type_id' => Type::TYPE_SIMPLE, + 'attribute_set_id' => 4, + 'sku' => 'test-simple-product', + 'name' => 'category 1', + 'price' => 150, + ], + ], + ]; + } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + */ + public function testUrlRewritesAfterProductDelete(): void + { + $product = $this->productRepository->get('simple2'); + $rewriteIds = $this->getEntityRewriteCollection($product->getId())->getAllIds(); + $this->productRepository->delete($product); + $this->assertEmpty( + array_intersect($this->getAllRewriteIds(), $rewriteIds), + 'Not all expected category url rewrites were deleted' + ); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testProductUrlRewritePerStoreViews(): void + { + $urlKeySecondStore = 'url-key-for-second-store'; + $secondStoreId = $this->storeRepository->get('fixture_second_store')->getId(); + $product = $this->productRepository->get('simple2'); + $urlKeyFirstStore = $product->getUrlKey(); + $product = $this->saveProduct( + ['store_id' => $secondStoreId, 'url_key' => $urlKeySecondStore], + $product + ); + $urlRewriteItems = $this->getEntityRewriteCollection($product->getId())->getItems(); + $this->assertTrue(count($urlRewriteItems) == 2); + foreach ($urlRewriteItems as $item) { + $item->getData('store_id') == $secondStoreId + ? $this->assertEquals($urlKeySecondStore . $this->suffix, $item->getRequestPath()) + : $this->assertEquals($urlKeyFirstStore . $this->suffix, $item->getRequestPath()); + } + } + + /** + * Save product with data using resource model directly + * + * @param array $data + * @param ProductInterface|null $product + * @return ProductInterface + */ + protected function saveProduct(array $data, $product = null): ProductInterface + { + $product = $product ?: $this->productFactory->create(); + $product->addData($data); + $this->productResource->save($product); + + return $product; + } + + /** + * @inheritdoc + */ + protected function getUrlSuffix(): string + { + return $this->suffix; + } + + /** + * @inheritdoc + */ + protected function getEntityType(): string + { + return DataProductUrlRewriteDatabaseMap::ENTITY_TYPE; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php index 0cd3ad644197b..8ca84c4b066fe 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php @@ -51,8 +51,8 @@ public function testGetAttributesMeta() $urlKeyData = $meta['search_engine_optimization']['children']['url_key']['arguments']['data']['config']; $this->assertEquals('text', $urlKeyData['dataType']); $this->assertEquals('input', $urlKeyData['formElement']); - $this->assertEquals('1', $urlKeyData['visible']); - $this->assertEquals('0', $urlKeyData['required']); + $this->assertEquals(true, $urlKeyData['visible']); + $this->assertEquals(false, $urlKeyData['required']); $this->assertEquals('[STORE VIEW]', $urlKeyData['scopeLabel']); } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php index 2695948e78314..85cd5331a29c4 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php @@ -172,4 +172,29 @@ public function createCollectionForSkuDataProvider() . '`attribute`:`sku`,`operator`:`!^[^]`,`value`:`virtual`^]^]', 'product-with-xss'] ]; } + + /** + * Check that collection returns correct result if use date attribute. + * + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_date_attribute.php + * @return void + */ + public function testProductListWithDateAttribute() + { + $encodedConditions = '^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,' + . '`aggregator`:`all`,`value`:`1`,`new_child`:``^],' + . '`1--1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Product`,' + . '`attribute`:`date_attribute`,`operator`:`==`,`value`:`' . date('Y-m-d') . '`^]^]'; + $this->block->setData('conditions_encoded', $encodedConditions); + + // Load products collection filtered using specified conditions and perform assertions + $productCollection = $this->block->createCollection(); + $productCollection->load(); + $this->assertEquals( + 1, + $productCollection->count(), + "Product collection was not filtered according to the widget condition." + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php index 4c653ab9ae33f..8d6c2625daadc 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\Product; use Magento\Checkout\Model\Session; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Serialize\Serializer\Json; @@ -81,6 +82,7 @@ public function testExecute($requestQuantity, $expectedResponse) ]; } + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($request); $this->dispatch('checkout/cart/updateItemQty'); $response = $this->getResponse()->getBody(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php index 440f437e74e7c..9b5650b1826c3 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Checkout\_files; use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; @@ -14,27 +16,29 @@ class ValidatorFileMock extends \PHPUnit\Framework\TestCase { /** * Returns mock. - * + * @param array|null $fileData * @return ValidatorFile|\PHPUnit_Framework_MockObject_MockObject */ - public function getInstance() + public function getInstance($fileData = null) { - $userValue = [ - 'type' => 'image/jpeg', - 'title' => "test.jpg", - 'quote_path' => "custom_options/quote/s/t/4624d2.jpg", - 'order_path' => "custom_options/order/s/t/89d25b4624d2.jpg", - "fullpath" => "pub/media/custom_options/quote/s/t/e47389d25b4624d2.jpg", - "size"=> "71901", - "width" => 5, - "height" => 5, - "secret_key" => "10839ec1631b77e5e473", - ]; + if (empty($fileData)) { + $fileData = [ + 'type' => 'image/jpeg', + 'title' => "test.jpg", + 'quote_path' => "custom_options/quote/s/t/4624d2.jpg", + 'order_path' => "custom_options/order/s/t/89d25b4624d2.jpg", + "fullpath" => "pub/media/custom_options/quote/s/t/e47389d25b4624d2.jpg", + "size" => "71901", + "width" => 5, + "height" => 5, + "secret_key" => "10839ec1631b77e5e473", + ]; + } $instance = $this->getMockBuilder(ValidatorFile::class) ->disableOriginalConstructor() ->getMock(); $instance->method('SetProduct')->willReturnSelf(); - $instance->method('validate')->willReturn($userValue); + $instance->method('validate')->willReturn($fileData); return $instance; } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php index e3e1513cb6144..0ae87725529b8 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php @@ -6,3 +6,6 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_1_not_default_store', 'reserved_order_id')->delete(); + +require __DIR__ . '/../../../Magento/Customer/_files/customer_rollback.php'; +require __DIR__ . '/../../../Magento/Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_downloadable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_downloadable_product_rollback.php new file mode 100644 index 0000000000000..4048c672037b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_downloadable_product_rollback.php @@ -0,0 +1,8 @@ +aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); + $this->pageRetriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); + $this->scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + parent::tearDown(); + + foreach ($this->pagesToDelete as $identifier) { + $page = $this->pageRetriever->execute($identifier); + $page->delete(); + } + $this->pagesToDelete = []; + } + + /** + * Check whether additional authorization is required for the design fields. + * + * @magentoDbIsolation disabled + * @return void + */ + public function testSaveDesign(): void + { + //Expected list of sessions messages collected throughout the controller calls. + $sessionMessages = ['You are not allowed to change CMS pages design settings']; + //Test page data. + $id = 'test-page' .rand(1111, 9999); + $requestData = [ + PageInterface::IDENTIFIER => $id, + PageInterface::TITLE => 'Page title', + PageInterface::CUSTOM_THEME => '1', + PageInterface::PAGE_LAYOUT => 'empty' + ]; + + //Creating a new page with design properties without the required permissions. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Cms::save_design'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->dispatch($this->uri); + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + + //Trying again with the permissions. + $this->aclBuilder->getAcl()->allow(null, ['Magento_Cms::save', 'Magento_Cms::save_design']); + $this->getRequest()->setDispatched(false); + $this->dispatch($this->uri); + /** @var Page $page */ + $page = Bootstrap::getObjectManager()->create(PageInterface::class); + $page->load($id, PageInterface::IDENTIFIER); + $this->assertNotEmpty($page->getId()); + $this->assertEquals(1, $page->getCustomTheme()); + $requestData['page_id'] = $page->getId(); + $this->getRequest()->setPostValue($requestData); + + //Updating the page without the permissions but not touching design settings. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Cms::save_design'); + $this->getRequest()->setDispatched(false); + $this->dispatch($this->uri); + $this->assertSessionMessages(self::equalTo($sessionMessages), MessageInterface::TYPE_ERROR); + + //Updating design settings without the permissions. + $requestData[PageInterface::CUSTOM_THEME] = '2'; + $this->getRequest()->setPostValue($requestData); + $this->getRequest()->setDispatched(false); + $this->dispatch($this->uri); + $sessionMessages[] = $sessionMessages[0]; + $this->assertSessionMessages( + self::equalTo($sessionMessages), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Check that default design values are accepted without the permissions. + * + * @magentoDbIsolation disabled + * @return void + */ + public function testSaveDesignWithDefaults(): void + { + //Test page data. + $id = 'test-page' .rand(1111, 9999); + $defaultLayout = $this->scopeConfig->getValue('web/default_layouts/default_cms_layout'); + $requestData = [ + PageInterface::IDENTIFIER => $id, + PageInterface::TITLE => 'Page title', + PageInterface::PAGE_LAYOUT => $defaultLayout + ]; + //Creating a new page with design properties without the required permissions but with default values. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Cms::save_design'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->dispatch($this->uri); + + //Validating saved page + /** @var Page $page */ + $page = Bootstrap::getObjectManager()->create(PageInterface::class); + $page->load($id, PageInterface::IDENTIFIER); + $this->assertNotEmpty($page->getId()); + $this->assertNotNull($page->getPageLayout()); + $this->assertEquals($defaultLayout, $page->getPageLayout()); + } + + /** + * Test that custom layout update fields are dealt with properly. + * + * @magentoDataFixture Magento/Cms/_files/pages_with_layout_xml.php + * @throws \Throwable + * @return void + */ + public function testSaveLayoutXml(): void + { + $page = $this->pageRetriever->execute('test_custom_layout_page_1', 0); + $requestData = [ + Page::PAGE_ID => $page->getId(), + PageInterface::IDENTIFIER => 'test_custom_layout_page_1', + PageInterface::TITLE => 'Page title', + PageInterface::CUSTOM_LAYOUT_UPDATE_XML => $page->getCustomLayoutUpdateXml(), + PageInterface::LAYOUT_UPDATE_XML => $page->getLayoutUpdateXml(), + 'layout_update_selected' => '_existing_' + ]; + + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->dispatch($this->uri); + $this->getRequest()->setDispatched(false); + + $updated = $this->pageRetriever->execute('test_custom_layout_page_1', 0); + $this->assertEquals($updated->getCustomLayoutUpdateXml(), $page->getCustomLayoutUpdateXml()); + $this->assertEquals($updated->getLayoutUpdateXml(), $page->getLayoutUpdateXml()); + + $requestData = [ + Page::PAGE_ID => $page->getId(), + PageInterface::IDENTIFIER => 'test_custom_layout_page_1', + PageInterface::TITLE => 'Page title', + PageInterface::CUSTOM_LAYOUT_UPDATE_XML => $page->getCustomLayoutUpdateXml(), + PageInterface::LAYOUT_UPDATE_XML => $page->getLayoutUpdateXml(), + 'layout_update_selected' => '_no_update_' + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + $this->dispatch($this->uri); + $this->getRequest()->setDispatched(false); + + $updated = $this->pageRetriever->execute('test_custom_layout_page_1', 0); + $this->assertEmpty($updated->getCustomLayoutUpdateXml()); + $this->assertEmpty($updated->getLayoutUpdateXml()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php index 8a0c715cfaf75..4600cd28fd3fc 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php @@ -9,6 +9,10 @@ */ namespace Magento\Cms\Controller; +use Magento\Cms\Api\GetPageByIdentifierInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; + class PageTest extends \Magento\TestFramework\TestCase\AbstractController { public function testViewAction() @@ -62,4 +66,23 @@ public static function cmsPageWithSystemRouteFixture() ->setPageLayout('1column') ->save(); } + + /** + * Check that custom handles are applied when rendering a page. + * + * @return void + * @throws \Throwable + * @magentoDataFixture Magento/Cms/_files/pages_with_layout_xml.php + */ + public function testCustomHandles(): void + { + /** @var GetPageByIdentifierInterface $pageFinder */ + $pageFinder = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); + $page = $pageFinder->execute('test_custom_layout_page_3', 0); + $this->dispatch('/cms/page/view/page_id/' .$page->getId()); + /** @var LayoutInterface $layout */ + $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $handles = $layout->getUpdate()->getHandles(); + $this->assertContains('cms_page_view_selectable_test_custom_layout_page_3_test_selected', $handles); + } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutManagerTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutManagerTest.php new file mode 100644 index 0000000000000..e741b95ff4371 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutManagerTest.php @@ -0,0 +1,102 @@ +resultFactory = $objectManager->get(PageResultFactory::class); + //Mocking available list of files for the page. + $handles = [ + 'cms_page_view_selectable_page100_select1', + 'cms_page_view_selectable_page100_select2' + ]; + $processor = $this->getMockBuilder(Merge::class)->disableOriginalConstructor()->getMock(); + $processor->method('getAvailableHandles')->willReturn($handles); + $processorFactory = $this->getMockBuilder(MergeFactory::class)->disableOriginalConstructor()->getMock(); + $processorFactory->method('create')->willReturn($processor); + + $this->manager = $objectManager->create( + CustomLayoutManagerInterface::class, + ['layoutProcessorFactory' => $processorFactory] + ); + $this->repo = $objectManager->create( + CustomLayoutRepositoryInterface::class, + ['manager' => $this->manager] + ); + $this->pageFactory = $objectManager->get(PageFactory::class); + $this->identityMap = $objectManager->get(IdentityMap::class); + } + + /** + * Test updating a page's custom layout. + * + * @magentoDataFixture Magento/Cms/_files/pages.php + * @throws \Throwable + * @return void + */ + public function testCustomLayoutUpdate(): void + { + /** @var Page $page */ + $page = $this->pageFactory->create(['customLayoutRepository' => $this->repo]); + $page->load('page100', 'identifier'); + $pageId = (int)$page->getId(); + $this->identityMap->add($page); + //Set file ID + $this->repo->save(new CustomLayoutSelected($pageId, 'select2')); + + //Test handles + $result = $this->resultFactory->create(); + $this->manager->applyUpdate($result, $this->repo->getFor($pageId)); + $this->identityMap->remove((int)$page->getId()); + $this->assertContains('___selectable_page100_select2', $result->getLayout()->getUpdate()->getHandles()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php new file mode 100644 index 0000000000000..e3422cd81638b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php @@ -0,0 +1,152 @@ +fakeManager = $objectManager->get(CustomLayoutManager::class); + $this->repo = $objectManager->create(CustomLayoutRepositoryInterface::class, ['manager' => $this->fakeManager]); + $this->pageFactory = $objectManager->get(PageFactory::class); + $this->identityMap = $objectManager->get(IdentityMap::class); + } + + /** + * Create page instance. + * + * @param string $id + * @return Page + */ + private function createPage(string $id): Page + { + $page = $this->pageFactory->create(['customLayoutRepository' => $this->repo]); + $page->load($id, 'identifier'); + $this->identityMap->add($page); + + return $page; + } + + /** + * Test updating a page's custom layout. + * + * @magentoDataFixture Magento/Cms/_files/pages.php + * @return void + */ + public function testCustomLayout(): void + { + $page = $this->createPage('page100'); + $pageId = (int)$page->getId(); + $this->fakeManager->fakeAvailableFiles($pageId, ['select1', 'select2']); + + //Invalid file ID + $exceptionRaised = null; + try { + $this->repo->save(new CustomLayoutSelected($pageId, 'some_file')); + } catch (\Throwable $exception) { + $exceptionRaised = $exception; + } + $this->assertNotEmpty($exceptionRaised); + $this->assertInstanceOf(\InvalidArgumentException::class, $exceptionRaised); + + //Set file ID + $this->repo->save(new CustomLayoutSelected($pageId, 'select2')); + + //Test saved + $saved = $this->repo->getFor($pageId); + $this->assertEquals('select2', $saved->getLayoutFileId()); + + //Removing custom file + $this->repo->deleteFor($pageId); + + //Test saved + $notFound = false; + try { + $this->repo->getFor($pageId); + } catch (NoSuchEntityException $exception) { + $notFound = true; + } + $this->assertTrue($notFound); + } + + /** + * Test that layout updates are saved with save method. + * + * @magentoDataFixture Magento/Cms/_files/pages.php + * @return void + */ + public function testSaved(): void + { + $page = $this->createPage('page100'); + $this->fakeManager->fakeAvailableFiles((int)$page->getId(), ['select1', 'select2']); + + //Special no-update instruction + $page->setData('layout_update_selected', '_no_update_'); + $page->save(); + $this->assertNull($page->getData('layout_update_selected')); + + //Existing file update + $page->setData('layout_update_selected', 'select1'); + $page->save(); + /** @var Page $page */ + $page = $this->pageFactory->create(); + $page->load('page100', 'identifier'); + $this->assertEquals('select1', $page->getData('layout_update_selected')); + $this->assertEquals('select1', $this->repo->getFor((int)$page->getId())->getLayoutFileId()); + + //Invalid file + $caught = null; + $page->setData('layout_update_selected', 'nonExisting'); + try { + $page->save(); + } catch (\Throwable $exception) { + $caught = $exception; + } + $this->assertInstanceOf(LocalizedException::class, $caught); + $this->assertEquals($caught->getMessage(), 'Invalid Custom Layout Update selected'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php new file mode 100644 index 0000000000000..2028f5d8a04b6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php @@ -0,0 +1,153 @@ +repo = $objectManager->get(GetPageByIdentifierInterface::class); + $this->filesFaker = $objectManager->get(CustomLayoutManager::class); + $this->request = $objectManager->get(HttpRequest::class); + $this->provider = $objectManager->create( + DataProvider::class, + [ + 'name' => 'test', + 'primaryFieldName' => 'page_id', + 'requestFieldName' => 'page_id', + 'customLayoutManager' => $this->filesFaker + ] + ); + } + + /** + * Check that custom layout date is handled properly. + * + * @magentoDataFixture Magento/Cms/_files/pages_with_layout_xml.php + * @throws \Throwable + * @return void + */ + public function testCustomLayoutData(): void + { + $data = $this->provider->getData(); + $page1Data = null; + $page2Data = null; + $page3Data = null; + foreach ($data as $pageData) { + if ($pageData[PageModel::IDENTIFIER] === 'test_custom_layout_page_1') { + $page1Data = $pageData; + } elseif ($pageData[PageModel::IDENTIFIER] === 'test_custom_layout_page_2') { + $page2Data = $pageData; + } elseif ($pageData[PageModel::IDENTIFIER] === 'test_custom_layout_page_3') { + $page3Data = $pageData; + } + } + $this->assertNotEmpty($page1Data); + $this->assertNotEmpty($page2Data); + $this->assertEquals('_existing_', $page1Data['layout_update_selected']); + $this->assertEquals(null, $page2Data['layout_update_selected']); + $this->assertEquals('test_selected', $page3Data['layout_update_selected']); + } + + /** + * Check that proper meta for custom layout field is returned. + * + * @return void + * @throws \Throwable + * @magentoDataFixture Magento/Cms/_files/pages_with_layout_xml.php + */ + public function testCustomLayoutMeta(): void + { + //Testing a page without layout xml + $page = $this->repo->execute('test_custom_layout_page_3', 0); + $this->filesFaker->fakeAvailableFiles((int)$page->getId(), ['test1', 'test2']); + $this->request->setParam('page_id', $page->getId()); + + $meta = $this->provider->getMeta(); + $this->assertArrayHasKey('design', $meta); + $this->assertArrayHasKey('children', $meta['design']); + $this->assertArrayHasKey('custom_layout_update_select', $meta['design']['children']); + $this->assertArrayHasKey('arguments', $meta['design']['children']['custom_layout_update_select']); + $this->assertArrayHasKey('data', $meta['design']['children']['custom_layout_update_select']['arguments']); + $this->assertArrayHasKey( + 'options', + $meta['design']['children']['custom_layout_update_select']['arguments']['data'] + ); + $expectedList = [ + ['label' => 'No update', 'value' => '_no_update_'], + ['label' => 'test1', 'value' => 'test1'], + ['label' => 'test2', 'value' => 'test2'] + ]; + $metaList = $meta['design']['children']['custom_layout_update_select']['arguments']['data']['options']; + sort($expectedList); + sort($metaList); + $this->assertEquals($expectedList, $metaList); + + //Page with old layout xml + $page = $this->repo->execute('test_custom_layout_page_1', 0); + $this->filesFaker->fakeAvailableFiles((int)$page->getId(), ['test3']); + $this->request->setParam('page_id', $page->getId()); + + $meta = $this->provider->getMeta(); + $this->assertArrayHasKey('design', $meta); + $this->assertArrayHasKey('children', $meta['design']); + $this->assertArrayHasKey('custom_layout_update_select', $meta['design']['children']); + $this->assertArrayHasKey('arguments', $meta['design']['children']['custom_layout_update_select']); + $this->assertArrayHasKey('data', $meta['design']['children']['custom_layout_update_select']['arguments']); + $this->assertArrayHasKey( + 'options', + $meta['design']['children']['custom_layout_update_select']['arguments']['data'] + ); + $expectedList = [ + ['label' => 'No update', 'value' => '_no_update_'], + ['label' => 'Use existing layout update XML', 'value' => '_existing_'], + ['label' => 'test3', 'value' => 'test3'], + ]; + $metaList = $meta['design']['children']['custom_layout_update_select']['arguments']['data']['options']; + sort($expectedList); + sort($metaList); + $this->assertEquals($expectedList, $metaList); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php index cd4674f95d722..5e7e0c962fcde 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php @@ -3,96 +3,81 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Cms\Model; -use Magento\Backend\Model\Auth; +use Magento\Cms\Api\GetPageByIdentifierInterface; use Magento\Cms\Api\PageRepositoryInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Magento\TestFramework\Bootstrap as TestBootstrap; -use Magento\Framework\Acl\Builder; /** - * Test class for page repository. + * Test page repo. */ class PageRepositoryTest extends TestCase { /** - * Test subject. - * * @var PageRepositoryInterface */ private $repo; /** - * @var Auth - */ - private $auth; - - /** - * @var SearchCriteriaBuilder + * @var GetPageByIdentifierInterface */ - private $criteriaBuilder; + private $retriever; /** - * @var Builder - */ - private $aclBuilder; - - /** - * Sets up common objects. - * * @inheritDoc */ protected function setUp() { - $this->repo = Bootstrap::getObjectManager()->create(PageRepositoryInterface::class); - $this->auth = Bootstrap::getObjectManager()->get(Auth::class); - $this->criteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); - $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); + $this->repo = Bootstrap::getObjectManager()->get(PageRepositoryInterface::class); + $this->retriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); } /** - * @inheritDoc - */ - protected function tearDown() - { - parent::tearDown(); - - $this->auth->logout(); - } - - /** - * Test authorization when saving page's design settings. + * Test that the field is deprecated. * - * @magentoDataFixture Magento/Cms/_files/pages.php - * @magentoAppArea adminhtml - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled + * @throws \Throwable + * @magentoDataFixture Magento/Cms/_files/pages_with_layout_xml.php + * @return void */ - public function testSaveDesign() + public function testSaveUpdateXml(): void { - $pages = $this->repo->getList( - $this->criteriaBuilder->addFilter('identifier', 'page_design_blank')->create() - )->getItems(); - $page = array_pop($pages); - $this->auth->login(TestBootstrap::ADMIN_NAME, TestBootstrap::ADMIN_PASSWORD); + $page = $this->retriever->execute('test_custom_layout_page_1', 0); + $page->setTitle($page->getTitle() .'TEST'); - //Admin doesn't have access to page's design. - $this->aclBuilder->getAcl()->deny(null, 'Magento_Cms::save_design'); - - $page->setCustomTheme('test'); + //Is successfully saved without changes to the custom layout xml. $page = $this->repo->save($page); - $this->assertNotEquals('test', $page->getCustomTheme()); - //Admin has access to page' design. - $this->aclBuilder->getAcl()->allow(null, ['Magento_Cms::save', 'Magento_Cms::save_design']); + //New value is not accepted. + $page->setCustomLayoutUpdateXml(''); + $forbidden = false; + try { + $page = $this->repo->save($page); + } catch (CouldNotSaveException $exception) { + $forbidden = true; + } + $this->assertTrue($forbidden); + + //New value is not accepted. + $page->setLayoutUpdateXml(''); + $forbidden = false; + try { + $page = $this->repo->save($page); + } catch (CouldNotSaveException $exception) { + $forbidden = true; + } + $this->assertTrue($forbidden); - $page->setCustomTheme('test'); + //Can be removed + $page->setCustomLayoutUpdateXml(null); + $page->setLayoutUpdateXml(null); $page = $this->repo->save($page); - $this->assertEquals('test', $page->getCustomTheme()); + $this->assertEmpty($page->getCustomLayoutUpdateXml()); + $this->assertEmpty($page->getLayoutUpdateXml()); } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php new file mode 100644 index 0000000000000..9734ed3abaeed --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php @@ -0,0 +1,47 @@ +get(PageModelFactory::class); +/** @var CustomLayoutManager $fakeManager */ +$fakeManager = $objectManager->get(CustomLayoutManager::class); +$layoutRepo = $objectManager->create(PageModel\CustomLayoutRepositoryInterface::class, ['manager' => $fakeManager]); + +/** @var PageModel $page */ +$page = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); +$page->setIdentifier('test_custom_layout_page_1'); +$page->setTitle('Test Page'); +$page->setCustomLayoutUpdateXml(''); +$page->setLayoutUpdateXml(''); +$page->setIsActive(true); +$page->setStoreId(0); +$page->save(); +/** @var PageModel $page2 */ +$page2 = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); +$page2->setIdentifier('test_custom_layout_page_2'); +$page2->setTitle('Test Page 2'); +$page->setIsActive(true); +$page->setStoreId(0); +$page2->save(); +/** @var PageModel $page3 */ +$page3 = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); +$page3->setIdentifier('test_custom_layout_page_3'); +$page3->setTitle('Test Page 3'); +$page3->setStores([0]); +$page3->setIsActive(1); +$page3->setContent('

Test Page

'); +$page3->setPageLayout('1column'); +$page3->save(); +$fakeManager->fakeAvailableFiles((int)$page3->getId(), ['test_selected']); +$page3->setData('layout_update_selected', 'test_selected'); +$page3->save(); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php new file mode 100644 index 0000000000000..3217b94d7392b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php @@ -0,0 +1,32 @@ +get(PageModelFactory::class); +/** @var PageModel $page */ +$page = $pageFactory->create(); +$page->load('test_custom_layout_page_1', PageModel::IDENTIFIER); +if ($page->getId()) { + $page->delete(); +} +/** @var PageModel $page2 */ +$page2 = $pageFactory->create(); +$page2->load('test_custom_layout_page_2', PageModel::IDENTIFIER); +if ($page2->getId()) { + $page2->delete(); +} +/** @var PageModel $page3 */ +$page3 = $pageFactory->create(); +$page3->load('test_custom_layout_page_3', PageModel::IDENTIFIER); +if ($page3->getId()) { + $page3->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml index f473af1bce37c..5b482e2a61ed0 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml @@ -13,7 +13,7 @@ Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded - If not specified, Default Country from General Config will be used + If not specified, Default Country from General Config will be used. Magento\Paypal\Block\Adminhtml\System\Config\Field\Country Magento\Paypal\Model\System\Config\Source\MerchantCountry Magento\Paypal\Model\System\Config\Backend\MerchantCountry diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/ConfigurableTest.php new file mode 100644 index 0000000000000..aba813148512c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/ConfigurableTest.php @@ -0,0 +1,68 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class) + ->createBlock(ConfigurableRenderer::class); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php + */ + public function testGetProductPriceHtml() + { + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->getById(1); + + $layout = $this->objectManager->get(LayoutInterface::class); + $layout->createBlock( + \Magento\Framework\Pricing\Render::class, + 'product.price.render.default', + [ + 'data' => [ + 'price_render_handle' => 'catalog_product_prices', + 'use_link_for_as_low_as' => true + ] + ] + ); + + $this->block->setItem( + $this->block->getCheckoutSession()->getQuote()->getAllVisibleItems()[0] + ); + $html = $this->block->getProductPriceHtml($configurableProduct); + $this->assertContains('$10.00', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 78fa4733a2562..0d2043434d359 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -254,6 +254,33 @@ public function testGetUsedProducts() } } + /** + * Tests the $requiredAttributes parameter; uses meta_description as an example of an attribute that is not + * included in default attribute select. + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php + */ + public function testGetUsedProductsWithRequiredAttributes() + { + $requiredAttributeIds = [86]; + $products = $this->model->getUsedProducts($this->product, $requiredAttributeIds); + foreach ($products as $product) { + self::assertNotNull($product->getData('meta_description')); + } + } + + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php + */ + public function testGetUsedProductsWithoutRequiredAttributes() + { + $products = $this->model->getUsedProducts($this->product); + foreach ($products as $product) { + self::assertNull($product->getData('meta_description')); + } + } + /** * Test getUsedProducts returns array with same indexes regardless collections was cache or not. * diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2.php new file mode 100644 index 0000000000000..4de079ba3b6ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2.php @@ -0,0 +1,62 @@ +get(\Magento\Eav\Model\Config::class); +$attribute2 = $eavConfig->getAttribute('catalog_product', 'test_configurable_2'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute2->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute2 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute2->setData( + [ + 'attribute_code' => 'test_configurable_2', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable 2'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attributeRepository->save($attribute2); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute2->getId()); +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2_rollback.php new file mode 100644 index 0000000000000..84f6ec58d3e4f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2_rollback.php @@ -0,0 +1,28 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_2'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php new file mode 100644 index 0000000000000..1bf816425a9c9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php @@ -0,0 +1,162 @@ +get(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute2->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [30, 40]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable2($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute2->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute2->getId(), + 'code' => $attribute2->getAttributeCode(), + 'label' => $attribute2->getStoreLabel(), + 'position' => '1', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(11) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('configurable_12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute_rollback.php new file mode 100644 index 0000000000000..d4fa2a97c4934 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute_rollback.php @@ -0,0 +1,14 @@ +unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +require __DIR__ . '/configurable_attribute_2_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php index 9a2f5c49ac298..70aa7c07ed536 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php @@ -121,7 +121,7 @@ $registry->register('isSecureArea', false); $product->setTypeId(Configurable::TYPE_CODE) - ->setId(11) + ->setId(111) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Product 12345') diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php new file mode 100644 index 0000000000000..d0afeeaf19fe8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php @@ -0,0 +1,145 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setMetaDescription('meta_description' . $productId) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +// Remove any previously created product with the same id. +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productToDelete = $productRepository->getById(1); + $productRepository->delete($productToDelete); + + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ + $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); + $itemResource->getConnection()->delete( + $itemResource->getMainTable(), + 'product_id = ' . $productToDelete->getId() + ); +} catch (\Exception $e) { + // Nothing to remove +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php new file mode 100644 index 0000000000000..21953dea6f587 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php @@ -0,0 +1,39 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspTest.php new file mode 100644 index 0000000000000..e66c6af36e42c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CspTest.php @@ -0,0 +1,164 @@ +getBody()), mb_strtolower($search)) !== false) { + return true; + } + + foreach ($response->getHeaders() as $header) { + if (mb_stripos(mb_strtolower($header->toString()), mb_strtolower($search)) !== false) { + return true; + } + } + + return false; + } + + /** + * Check that configured policies are rendered on frontend. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/policies/storefront/default_src/policy_id default-src + * @magentoConfigFixture default_store csp/policies/storefront/default_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/script_src/policy_id script-src + * @magentoConfigFixture default_store csp/policies/storefront/script_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/script_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/script_src/inline 1 + * @magentoConfigFixture default_store csp/policies/admin/font_src/policy_id font-src + * @magentoConfigFixture default_store csp/policies/admin/font_src/none 0 + * @magentoConfigFixture default_store csp/policies/admin/font_src/self 1 + * @return void + */ + public function testStorefrontPolicies(): void + { + $this->dispatch('/'); + $response = $this->getResponse(); + + $this->assertTrue($this->searchInResponse($response, 'Content-Security-Policy')); + $this->assertTrue($this->searchInResponse($response, 'default-src')); + $this->assertTrue($this->searchInResponse($response, 'http://magento.com')); + $this->assertTrue($this->searchInResponse($response, 'http://devdocs.magento.com')); + $this->assertTrue($this->searchInResponse($response, '\'self\'')); + $this->assertFalse($this->searchInResponse($response, '\'none\'')); + $this->assertTrue($this->searchInResponse($response, 'script-src')); + $this->assertTrue($this->searchInResponse($response, '\'unsafe-inline\'')); + $this->assertFalse($this->searchInResponse($response, 'font-src')); + //Policies configured in cps_whitelist.xml files + $this->assertTrue($this->searchInResponse($response, 'object-src')); + $this->assertTrue($this->searchInResponse($response, 'media-src')); + } + + /** + * Check that configured policies are rendered on backend. + * + * @magentoAppArea adminhtml + * @magentoConfigFixture default_store csp/policies/admin/default_src/policy_id default-src + * @magentoConfigFixture default_store csp/policies/admin/default_src/none 0 + * @magentoConfigFixture default_store csp/policies/admin/default_src/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/admin/default_src/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/admin/default_src/self 1 + * @magentoConfigFixture default_store csp/policies/admin/script_src/policy_id script-src + * @magentoConfigFixture default_store csp/policies/admin/script_src/none 0 + * @magentoConfigFixture default_store csp/policies/admin/default_src/self 1 + * @magentoConfigFixture default_store csp/policies/admin/default_src/inline 1 + * @magentoConfigFixture default_store csp/policies/storefront/font_src/policy_id font-src + * @magentoConfigFixture default_store csp/policies/storefront/font_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/font_src/self 1 + * @return void + */ + public function testAdminPolicies(): void + { + $this->dispatch('backend/'); + $response = $this->getResponse(); + + $this->assertTrue($this->searchInResponse($response, 'Content-Security-Policy')); + $this->assertTrue($this->searchInResponse($response, 'default-src')); + $this->assertTrue($this->searchInResponse($response, 'http://magento.com')); + $this->assertTrue($this->searchInResponse($response, 'http://devdocs.magento.com')); + $this->assertTrue($this->searchInResponse($response, '\'self\'')); + $this->assertFalse($this->searchInResponse($response, '\'none\'')); + $this->assertTrue($this->searchInResponse($response, 'script-src')); + $this->assertTrue($this->searchInResponse($response, '\'unsafe-inline\'')); + $this->assertFalse($this->searchInResponse($response, 'font-src')); + } + + /** + * Check that CSP mode is considered when rendering policies. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/policies/storefront/default_src/policy_id default-src + * @magentoConfigFixture default_store csp/policies/storefront/default_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/self 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_only 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri /cspEndpoint/ + * @magentoConfigFixture default_store csp/mode/admin/report_only 0 + * @return void + */ + public function testReportOnlyMode(): void + { + $this->dispatch('/'); + $response = $this->getResponse(); + + $this->assertTrue($this->searchInResponse($response, 'Content-Security-Policy-Report-Only')); + $this->assertTrue($this->searchInResponse($response, '/cspEndpoint/')); + $this->assertTrue($this->searchInResponse($response, 'default-src')); + $this->assertTrue($this->searchInResponse($response, 'http://magento.com')); + $this->assertTrue($this->searchInResponse($response, 'http://devdocs.magento.com')); + $this->assertTrue($this->searchInResponse($response, '\'self\'')); + } + + /** + * Check that CSP reporting options are rendered for 'restrict' mode as well. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/policies/storefront/default_src/policy_id default-src + * @magentoConfigFixture default_store csp/policies/storefront/default_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/self 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri /cspEndpoint/ + * @magentoConfigFixture default_store csp/mode/admin/report_only 0 + * @return void + */ + public function testRestrictMode(): void + { + $this->dispatch('/'); + $response = $this->getResponse(); + + $this->assertFalse($this->searchInResponse($response, 'Content-Security-Policy-Report-Only')); + $this->assertTrue($this->searchInResponse($response, 'Content-Security-Policy')); + $this->assertTrue($this->searchInResponse($response, '/cspEndpoint/')); + $this->assertTrue($this->searchInResponse($response, 'default-src')); + $this->assertTrue($this->searchInResponse($response, 'http://magento.com')); + $this->assertTrue($this->searchInResponse($response, 'http://devdocs.magento.com')); + $this->assertTrue($this->searchInResponse($response, '\'self\'')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php new file mode 100644 index 0000000000000..6d8876012df1e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php @@ -0,0 +1,186 @@ +collector = Bootstrap::getObjectManager()->get(ConfigCollector::class); + } + + /** + * Create expected policy objects. + * + * @return PolicyInterface[] + */ + private function getExpectedPolicies(): array + { + return [ + 'child-src' => new FetchPolicy( + 'child-src', + false, + ['http://magento.com', 'http://devdocs.magento.com'], + ['http'], + true, + true, + false, + [], + [], + true + ), + 'child-src2' => new FetchPolicy('child-src', false, [], [], false, false, true), + 'connect-src' => new FetchPolicy('connect-src'), + 'default-src' => new FetchPolicy( + 'default-src', + false, + ['http://magento.com', 'http://devdocs.magento.com'], + [], + true + ), + 'font-src' => new FetchPolicy('font-src', false, [], [], true), + 'frame-src' => new FetchPolicy('frame-src', false, [], [], true, false, false, [], [], true), + 'img-src' => new FetchPolicy('img-src', false, [], [], true), + 'manifest-src' => new FetchPolicy('manifest-src', false, [], [], true), + 'media-src' => new FetchPolicy('media-src', false, [], [], true), + 'object-src' => new FetchPolicy('object-src', false, [], [], true), + 'script-src' => new FetchPolicy('script-src', false, [], [], true, false, false, [], [], false, true), + 'style-src' => new FetchPolicy('style-src', false, [], [], true), + 'base-uri' => new FetchPolicy('base-uri', false, [], [], true), + 'plugin-types' => new PluginTypesPolicy( + ['application/x-shockwave-flash', 'application/x-java-applet'] + ), + 'sandbox' => new SandboxPolicy(true, true, true, true, false, false, true, true, true, true, true), + 'form-action' => new FetchPolicy('form-action', false, [], [], true), + 'frame-ancestors' => new FetchPolicy('frame-ancestors', false, [], [], true), + 'block-all-mixed-content' => new FlagPolicy('block-all-mixed-content'), + 'upgrade-insecure-requests' => new FlagPolicy('upgrade-insecure-requests') + ]; + } + + /** + * Test initiating policies from config. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/policies/storefront/default_src/policy_id default-src + * @magentoConfigFixture default_store csp/policies/storefront/default_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/child_src/policy_id child-src + * @magentoConfigFixture default_store csp/policies/storefront/child_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/child_src/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/child_src/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/child_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/child_src/inline 1 + * @magentoConfigFixture default_store csp/policies/storefront/child_src/schemes/scheme1 http + * @magentoConfigFixture default_store csp/policies/storefront/child_src/dynamic 1 + * @magentoConfigFixture default_store csp/policies/storefront/child_src2/policy_id child-src + * @magentoConfigFixture default_store csp/policies/storefront/child_src2/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/child_src2/eval 1 + * @magentoConfigFixture default_store csp/policies/storefront/connect_src/policy_id connect-src + * @magentoConfigFixture default_store csp/policies/storefront/connect_src/none 1 + * @magentoConfigFixture default_store csp/policies/storefront/font_src/policy_id font-src + * @magentoConfigFixture default_store csp/policies/storefront/font_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/font_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frame_src/policy_id frame-src + * @magentoConfigFixture default_store csp/policies/storefront/frame_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/frame_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frame_src/dynamic 1 + * @magentoConfigFixture default_store csp/policies/storefront/img_src/policy_id img-src + * @magentoConfigFixture default_store csp/policies/storefront/img_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/img_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/policy_id manifest-src + * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/media_src/policy_id media-src + * @magentoConfigFixture default_store csp/policies/storefront/media_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/media_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/object_src/policy_id object-src + * @magentoConfigFixture default_store csp/policies/storefront/object_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/object_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/script_src/policy_id script-src + * @magentoConfigFixture default_store csp/policies/storefront/script_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/script_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/script_src/event_handlers 1 + * @magentoConfigFixture default_store csp/policies/storefront/base_uri/policy_id base-uri + * @magentoConfigFixture default_store csp/policies/storefront/base_uri/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/base_uri/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/style_src/policy_id style-src + * @magentoConfigFixture default_store csp/policies/storefront/style_src/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/style_src/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/form_action/policy_id form-action + * @magentoConfigFixture default_store csp/policies/storefront/form_action/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/form_action/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/policy_id frame-ancestors + * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/policy_id plugin-types + * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/types/fl application/x-shockwave-flash + * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/types/applet application/x-java-applet + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/policy_id sandbox + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/forms 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/modals 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/orientation 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/pointer 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/popup 0 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/popups_to_escape 0 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/presentation 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/same_origin 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/scripts 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/navigation 1 + * @magentoConfigFixture default_store csp/policies/storefront/sandbox/navigation_by_user 1 + * @magentoConfigFixture default_store csp/policies/storefront/mixed_content/policy_id block-all-mixed-content + * @magentoConfigFixture default_store csp/policies/storefront/upgrade/policy_id upgrade-insecure-requests + * @return void + */ + public function testCollecting(): void + { + $policies = $this->collector->collect([new FlagPolicy('upgrade-insecure-requests')]); + $checked = []; + $expectedPolicies = $this->getExpectedPolicies(); + + //Policies were collected + $this->assertNotEmpty($policies); + //Default policies are being kept + /** @var PolicyInterface $defaultPolicy */ + $defaultPolicy = array_shift($policies); + $this->assertEquals('upgrade-insecure-requests', $defaultPolicy->getId()); + //Comparing collected with configured + /** @var PolicyInterface $policy */ + foreach ($policies as $policy) { + $id = $policy->getId(); + if ($id === 'child-src' && $policy->isEvalAllowed()) { + $id = 'child-src2'; + } + $this->assertEquals($expectedPolicies[$id], $policy); + $checked[] = $id; + } + $expectedIds = array_keys($expectedPolicies); + $this->assertEquals(sort($expectedIds), sort($checked)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php new file mode 100644 index 0000000000000..bbaabba9dd268 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php @@ -0,0 +1,68 @@ +collector = Bootstrap::getObjectManager()->get(CspWhitelistXmlCollector::class); + } + + /** + * Test collecting configurations from multiple XML files. + * + * @return void + */ + public function testCollecting(): void + { + $policies = $this->collector->collect([]); + + $mediaSrcChecked = false; + $objectSrcChecked = false; + $this->assertNotEmpty($policies); + /** @var FetchPolicy $policy */ + foreach ($policies as $policy) { + $this->assertFalse($policy->isNoneAllowed()); + $this->assertFalse($policy->isSelfAllowed()); + $this->assertFalse($policy->isInlineAllowed()); + $this->assertFalse($policy->isEvalAllowed()); + $this->assertFalse($policy->isDynamicAllowed()); + $this->assertEmpty($policy->getSchemeSources()); + $this->assertEmpty($policy->getNonceValues()); + if ($policy->getId() === 'object-src') { + $this->assertInstanceOf(FetchPolicy::class, $policy); + $this->assertEquals(['http://magento.com', 'https://devdocs.magento.com'], $policy->getHostSources()); + $this->assertEquals(['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], $policy->getHashes()); + $objectSrcChecked = true; + } elseif ($policy->getId() === 'media-src') { + $this->assertInstanceOf(FetchPolicy::class, $policy); + $this->assertEquals(['https://magento.com', 'https://devdocs.magento.com'], $policy->getHostSources()); + $this->assertEmpty($policy->getHashes()); + $mediaSrcChecked = true; + } + } + $this->assertTrue($objectSrcChecked); + $this->assertTrue($mediaSrcChecked); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/CompositePolicyCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/CompositePolicyCollectorTest.php new file mode 100644 index 0000000000000..fd0c58235de1d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/CompositePolicyCollectorTest.php @@ -0,0 +1,177 @@ +getMockForAbstractClass(PolicyCollectorInterface::class); + $mockCollector1->method('collect') + ->willReturnCallback( + function (array $prevPolicies) { + return array_merge( + $prevPolicies, + [ + new FetchPolicy( + 'script-src', + false, + ['https://magento.com'], + ['https'], + true, + false, + true, + ['569403695046645'], + ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], + false, + true + ), + new FetchPolicy('script-src', false, ['https://devdocs.magento.com']), + new FlagPolicy('upgrade-insecure-requests'), + new PluginTypesPolicy(['application/x-shockwave-flash']), + new SandboxPolicy(false, true, false, true, false, true, false, true, false, true, false) + ] + ); + } + ); + $mockCollector2 = $this->getMockForAbstractClass(PolicyCollectorInterface::class); + $mockCollector2->method('collect') + ->willReturnCallback( + function (array $prevPolicies) { + return array_merge( + $prevPolicies, + [ + new FetchPolicy( + 'script-src', + true, + ['http://magento.com'], + ['http'], + false, + false, + false, + ['5694036950466451'], + ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF7=' => 'sha256'], + true, + false + ), + new FetchPolicy('default-src', false, [], [], true), + new FlagPolicy('upgrade-insecure-requests'), + new PluginTypesPolicy(['application/x-java-applet']), + new SandboxPolicy(true, false, true, false, true, false, true, false, true, false, false) + ] + ); + } + ); + + return [$mockCollector1, $mockCollector2]; + } + + /** + * Test collect method. + * + * Supply fake collectors, check results. + * + * @return void + */ + public function testCollect(): void + { + /** @var CompositePolicyCollector $collector */ + $collector = Bootstrap::getObjectManager()->create( + CompositePolicyCollector::class, + ['collectors' => $this->createMockCollectors()] + ); + + $collected = $collector->collect([]); + /** @var FetchPolicy[]|FlagPolicy[]|PluginTypesPolicy[]|SandboxPolicy[] $policies */ + $policies = []; + /** @var \Magento\Csp\Api\Data\PolicyInterface $policy */ + foreach ($collected as $policy) { + $policies[$policy->getId()] = $policy; + } + //Comparing resulting policies + $this->assertArrayHasKey('script-src', $policies); + $this->assertTrue($policies['script-src']->isNoneAllowed()); + $this->assertTrue($policies['script-src']->isSelfAllowed()); + $this->assertFalse($policies['script-src']->isInlineAllowed()); + $this->assertTrue($policies['script-src']->isEvalAllowed()); + $this->assertTrue($policies['script-src']->isDynamicAllowed()); + $this->assertTrue($policies['script-src']->areEventHandlersAllowed()); + $foundHosts = $policies['script-src']->getHostSources(); + $hosts = ['http://magento.com', 'https://magento.com', 'https://devdocs.magento.com']; + sort($foundHosts); + sort($hosts); + $this->assertEquals($hosts, $foundHosts); + $foundSchemes = $policies['script-src']->getSchemeSources(); + $schemes = ['https', 'http']; + sort($foundSchemes); + sort($schemes); + $this->assertEquals($schemes, $foundSchemes); + $foundNonceValues = $policies['script-src']->getNonceValues(); + $nonceValues = ['5694036950466451', '569403695046645']; + sort($foundNonceValues); + sort($nonceValues); + $this->assertEquals($nonceValues, $foundNonceValues); + $foundHashes = $policies['script-src']->getHashes(); + $hashes = [ + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF7=' => 'sha256', + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256' + ]; + $this->assertEquals($hashes, $foundHashes); + + $this->assertArrayHasKey('default-src', $policies); + $this->assertFalse($policies['default-src']->isNoneAllowed()); + $this->assertTrue($policies['default-src']->isSelfAllowed()); + $this->assertFalse($policies['default-src']->isInlineAllowed()); + $this->assertFalse($policies['default-src']->isEvalAllowed()); + $this->assertFalse($policies['default-src']->isDynamicAllowed()); + $this->assertFalse($policies['default-src']->areEventHandlersAllowed()); + $this->assertEmpty($policies['default-src']->getHashes()); + $this->assertEmpty($policies['default-src']->getNonceValues()); + $this->assertEmpty($policies['default-src']->getHostSources()); + $this->assertEmpty($policies['default-src']->getSchemeSources()); + + $this->assertArrayHasKey('upgrade-insecure-requests', $policies); + $this->assertInstanceOf(FlagPolicy::class, $policies['upgrade-insecure-requests']); + + $this->assertArrayHasKey('plugin-types', $policies); + $types = ['application/x-java-applet', 'application/x-shockwave-flash']; + $foundTypes = $policies['plugin-types']->getTypes(); + sort($types); + sort($foundTypes); + $this->assertEquals($types, $foundTypes); + + $this->assertArrayHasKey('sandbox', $policies); + $this->assertTrue($policies['sandbox']->isFormAllowed()); + $this->assertTrue($policies['sandbox']->isModalsAllowed()); + $this->assertTrue($policies['sandbox']->isOrientationLockAllowed()); + $this->assertTrue($policies['sandbox']->isPointerLockAllowed()); + $this->assertTrue($policies['sandbox']->isPopupsAllowed()); + $this->assertTrue($policies['sandbox']->isPopupsToEscapeSandboxAllowed()); + $this->assertTrue($policies['sandbox']->isScriptsAllowed()); + $this->assertFalse($policies['sandbox']->isTopNavigationByUserActivationAllowed()); + $this->assertTrue($policies['sandbox']->isTopNavigationAllowed()); + $this->assertTrue($policies['sandbox']->isSameOriginAllowed()); + $this->assertTrue($policies['sandbox']->isPresentationAllowed()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Mode/ConfigManagerTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Mode/ConfigManagerTest.php new file mode 100644 index 0000000000000..44790ef9dbc94 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Mode/ConfigManagerTest.php @@ -0,0 +1,86 @@ +manager = Bootstrap::getObjectManager()->get(ConfigManager::class); + } + + /** + * Check the default configurations of CSP. + * + * @magentoAppArea frontend + * @return void + */ + public function testStorefrontDefault(): void + { + $config = $this->manager->getConfigured(); + $this->assertTrue($config->isReportOnly()); + $this->assertNull($config->getReportUri()); + } + + /** + * Check the default configurations of CSP. + * + * @magentoAppArea adminhtml + * @return void + */ + public function testAdminDefault(): void + { + $config = $this->manager->getConfigured(); + $this->assertTrue($config->isReportOnly()); + $this->assertNull($config->getReportUri()); + } + + /** + * Check that class returns correct configurations. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri https://magento.com + * @return void + */ + public function testFrontendConfigured(): void + { + $config = $this->manager->getConfigured(); + $this->assertFalse($config->isReportOnly()); + $this->assertEquals('https://magento.com', $config->getReportUri()); + } + + /** + * Check that class returns correct configurations. + * + * @magentoAppArea adminhtml + * @magentoConfigFixture default_store csp/mode/admin/report_only 0 + * @magentoConfigFixture default_store csp/mode/admin/report_uri https://magento.com + * @return void + */ + public function testAdminConfigured(): void + { + $config = $this->manager->getConfigured(); + $this->assertFalse($config->isReportOnly()); + $this->assertEquals('https://magento.com', $config->getReportUri()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php new file mode 100644 index 0000000000000..a67665c6d3c48 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php @@ -0,0 +1,147 @@ +renderer = Bootstrap::getObjectManager()->get(SimplePolicyHeaderRenderer::class); + $this->response = Bootstrap::getObjectManager()->create(HttpResponse::class); + } + + /** + * Test policy rendering in restrict mode. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri 0 + * + * @return void + */ + public function testRenderRestrictMode(): void + { + $policy = new FetchPolicy('default-src', false, ['https://magento.com'], [], true); + + $this->renderer->render($policy, $this->response); + + $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy')); + $this->assertEmpty($this->response->getHeader('Content-Security-Policy-Report-Only')); + $this->assertEquals('default-src https://magento.com \'self\';', $header->getFieldValue()); + } + + /** + * Test policy rendering in restrict mode with report URL provided. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri /csp-reports/ + * + * @return void + */ + public function testRenderRestrictWithReportingMode(): void + { + $policy = new FetchPolicy('default-src', false, ['https://magento.com'], [], true); + + $this->renderer->render($policy, $this->response); + + $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy')); + $this->assertEmpty($this->response->getHeader('Content-Security-Policy-Report-Only')); + $this->assertEquals( + 'default-src https://magento.com \'self\'; report-uri /csp-reports/; report-to report-endpoint;', + $header->getFieldValue() + ); + $this->assertNotEmpty($reportToHeader = $this->response->getHeader('Report-To')); + $this->assertNotEmpty($reportData = json_decode("[{$reportToHeader->getFieldValue()}]", true)); + $this->assertEquals('report-endpoint', $reportData[0]['group']); + } + + /** + * Test policy rendering in report-only mode. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri 0 + * + * @return void + */ + public function testRenderReportMode(): void + { + $policy = new FetchPolicy( + 'default-src', + false, + ['https://magento.com'], + ['https'], + true, + true, + true, + ['5749837589457695'], + ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], + true, + true + ); + + $this->renderer->render($policy, $this->response); + + $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy-Report-Only')); + $this->assertEmpty($this->response->getHeader('Content-Security-Policy')); + $this->assertEquals( + 'default-src https://magento.com https: \'self\' \'unsafe-inline\' \'unsafe-eval\' \'strict-dynamic\'' + . ' \'unsafe-hashes\' \'nonce-'.base64_encode($policy->getNonceValues()[0]).'\'' + . ' \'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=\';', + $header->getFieldValue() + ); + } + + /** + * Test policy rendering in report-only mode with report URL provided. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri /csp-reports/ + * + * @return void + */ + public function testRenderReportWithReportingMode(): void + { + $policy = new FetchPolicy('default-src', false, ['https://magento.com'], [], true); + + $this->renderer->render($policy, $this->response); + + $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy-Report-Only')); + $this->assertEmpty($this->response->getHeader('Content-Security-Policy')); + $this->assertEquals( + 'default-src https://magento.com \'self\'; report-uri /csp-reports/; report-to report-endpoint;', + $header->getFieldValue() + ); + $this->assertNotEmpty($reportToHeader = $this->response->getHeader('Report-To')); + $this->assertNotEmpty($reportData = json_decode("[{$reportToHeader->getFieldValue()}]", true)); + $this->assertEquals('report-endpoint', $reportData[0]['group']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php index 8e25e5960a4b5..15111b27783d9 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php @@ -6,11 +6,30 @@ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\Escaper; + /** * Fetch Rates Test */ class FetchRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var Escaper + */ + private $escaper; + + /** + * Initial setup + */ + protected function setUp() + { + $this->escaper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Escaper::class + ); + + parent::setUp(); + } + /** * Test fetch action without service * @@ -46,7 +65,11 @@ public function testFetchRatesActionWithNonexistentService(): void $this->dispatch('backend/admin/system_currency/fetchRates'); $this->assertSessionMessages( - $this->contains("The import model can't be initialized. Verify the model and try again."), + $this->contains( + $this->escaper->escapeHtml( + "The import model can't be initialized. Verify the model and try again." + ) + ), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php index fefd1a7b250c3..536aadd190c0e 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Escaper; class SaveRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -13,6 +15,11 @@ class SaveRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendContr /** @var \Magento\Directory\Model\Currency $currencyRate */ protected $currencyRate; + /** + * @var Escaper + */ + private $escaper; + /** * Initial setup */ @@ -21,6 +28,10 @@ protected function setUp() $this->currencyRate = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Directory\Model\Currency::class ); + $this->escaper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Escaper::class + ); + parent::setUp(); } @@ -89,7 +100,9 @@ public function testSaveWithWarningAction() $this->assertSessionMessages( $this->contains( - (string)__('Please correct the input data for "%1 => %2" rate.', $currencyCode, $currencyTo) + $this->escaper->escapeHtml( + (string)__('Please correct the input data for "%1 => %2" rate.', $currencyCode, $currencyTo) + ) ), \Magento\Framework\Message\MessageInterface::TYPE_WARNING ); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/Dashboard/AddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/Dashboard/AddressTest.php index b159dceadfb77..c5b76807f1ff9 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/Dashboard/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/Dashboard/AddressTest.php @@ -69,7 +69,7 @@ public function testGetCustomer() public function testGetCustomerMissingCustomer() { - $moduleManager = $this->objectManager->get(\Magento\Framework\Module\ModuleManagerInterface::class); + $moduleManager = $this->objectManager->get(\Magento\Framework\Module\Manager::class); if ($moduleManager->isEnabled('Magento_PageCache')) { $customerDataFactory = $this->objectManager->create( \Magento\Customer\Api\Data\CustomerInterfaceFactory::class diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index 566dfbadedd29..df4acf3acca91 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Account\Redirect; +use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\Session; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -20,11 +21,14 @@ use Magento\Framework\Message\MessageInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Store\Model\StoreManager; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\Request; use Magento\TestFramework\Response; use Magento\Theme\Controller\Result\MessagePlugin; +use PHPUnit\Framework\Constraint\StringContains; use Zend\Stdlib\Parameters; /** @@ -744,6 +748,65 @@ public function testLoginPostRedirect($redirectDashboard, string $redirectUrl) $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); } + /** + * Register Customer with email confirmation. + * + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php + * @return void + */ + public function testRegisterCustomerWithEmailConfirmation(): void + { + $email = 'test_example@email.com'; + $this->fillRequestWithAccountDataAndFormKey($email); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $this->assertSessionMessages( + $this->equalTo( + [ + 'You must confirm your account. Please check your email for the confirmation link or ' + . 'click here for a new link.' + ] + ), + MessageInterface::TYPE_SUCCESS + ); + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); + /** @var CustomerInterface $customer */ + $customer = $customerRepository->get($email); + $confirmation = $customer->getConfirmation(); + $message = $this->transportBuilderMock->getSentMessage(); + $rawMessage = $message->getBody()->getParts()[0]->getRawContent(); + $messageConstraint = $this->logicalAnd( + new StringContains("You must confirm your {$email} email before you can sign in (link is only valid once"), + new StringContains("customer/account/confirm/?id={$customer->getId()}&key={$confirmation}") + ); + $this->assertThat($rawMessage, $messageConstraint); + + /** @var CookieManagerInterface $cookieManager */ + $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME); + $this->_objectManager->removeSharedInstance(Http::class); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_request = null; + + $this->getRequest() + ->setParam('id', $customer->getId()) + ->setParam('key', $confirmation); + $this->dispatch('customer/account/confirm'); + + /** @var StoreManager $store */ + $store = $this->_objectManager->get(StoreManagerInterface::class); + $name = $store->getStore()->getFrontendName(); + + $this->assertRedirect($this->stringContains('customer/account/index/')); + $this->assertSessionMessages( + $this->equalTo(["Thank you for registering with {$name}."]), + MessageInterface::TYPE_SUCCESS + ); + $this->assertEmpty($customerRepository->get($email)->getConfirmation()); + } + /** * Test that confirmation email address displays special characters correctly. * @@ -769,9 +832,21 @@ public function testConfirmationEmailWithSpecialCharacters(): void $message = $this->transportBuilderMock->getSentMessage(); $rawMessage = $message->getRawMessage(); - $this->assertContains('To: John Smith <' . $email . '>', $rawMessage); + /** @var \Zend\Mime\Part $messageBodyPart */ + $messageBodyParts = $message->getBody()->getParts(); + $messageBodyPart = reset($messageBodyParts); + $messageEncoding = $messageBodyPart->getCharset(); + $name = 'John Smith'; + + if (strtoupper($messageEncoding) !== 'ASCII') { + $name = \Zend\Mail\Header\HeaderWrap::mimeEncodeValue($name, $messageEncoding); + } + + $nameEmail = sprintf('%s <%s>', $name, $email); + + $this->assertContains('To: ' . $nameEmail, $rawMessage); - $content = $message->getBody()->getParts()[0]->getRawContent(); + $content = $messageBodyPart->getRawContent(); $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); $this->setRequestInfo($confirmationUrl, 'confirm'); $this->clearCookieMessagesList(); @@ -784,6 +859,141 @@ public function testConfirmationEmailWithSpecialCharacters(): void ); } + /** + * Check that Customer which change email can't log in with old email. + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/captcha/enable 0 + * + * @return void + */ + public function testResetPasswordWhenEmailChanged(): void + { + $email = 'customer@example.com'; + $newEmail = 'new_customer@example.com'; + + /* Reset password and check mail with token */ + $this->getRequest()->setPostValue(['email' => $email]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + + $this->dispatch('customer/account/forgotPasswordPost'); + $this->assertRedirect($this->stringContains('customer/account/')); + $this->assertSessionMessages( + $this->equalTo( + [ + "If there is an account associated with {$email} you will receive an email with a link " + . "to reset your password." + ] + ), + MessageInterface::TYPE_SUCCESS + ); + + /** @var CustomerRegistry $customerRegistry */ + $customerRegistry = $this->_objectManager->get(CustomerRegistry::class); + $customerData = $customerRegistry->retrieveByEmail($email); + $token = $customerData->getRpToken(); + $this->assertForgotPasswordEmailContent($token); + + /* Set new email */ + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $customerRepository->getById($customerData->getId()); + $customer->setEmail($newEmail); + $customerRepository->save($customer); + + /* Goes through the link in a mail */ + $this->resetRequest(); + $this->getRequest() + ->setParam('token', $token) + ->setParam('id', $customerData->getId()); + + $this->dispatch('customer/account/createPassword'); + + $this->assertRedirect($this->stringContains('customer/account/forgotpassword')); + $this->assertSessionMessages( + $this->equalTo(['Your password reset link has expired.']), + MessageInterface::TYPE_ERROR + ); + /* Trying to log in with old email */ + $this->resetRequest(); + $this->clearCookieMessagesList(); + $customerRegistry->removeByEmail($email); + + $this->dispatchLoginPostAction($email, 'password'); + $this->assertSessionMessages( + $this->equalTo( + [ + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ] + ), + MessageInterface::TYPE_ERROR + ); + $this->assertRedirect($this->stringContains('customer/account/login')); + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $this->assertFalse($session->isLoggedIn()); + + /* Trying to log in with correct(new) email */ + $this->resetRequest(); + $this->dispatchLoginPostAction($newEmail, 'password'); + $this->assertRedirect($this->stringContains('customer/account/')); + $this->assertTrue($session->isLoggedIn()); + $session->logout(); + } + + /** + * Set needed parameters and dispatch Customer loginPost action. + * + * @param string $email + * @param string $password + * @return void + */ + private function dispatchLoginPostAction(string $email, string $password): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue( + [ + 'login' => [ + 'username' => $email, + 'password' => $password, + ], + ] + ); + $this->dispatch('customer/account/loginPost'); + } + + /** + * Check that 'Forgot password' email contains correct data. + * + * @param string $token + * @return void + */ + private function assertForgotPasswordEmailContent(string $token): void + { + $message = $this->transportBuilderMock->getSentMessage(); + $pattern = "//"; + $rawMessage = $message->getBody()->getParts()[0]->getRawContent(); + $messageConstraint = $this->logicalAnd( + new StringContains('There was recently a request to change the password for your account.'), + $this->matchesRegularExpression($pattern) + ); + $this->assertThat($rawMessage, $messageConstraint); + } + + /** + * Clear request object. + * + * @return void + */ + private function resetRequest(): void + { + $this->_objectManager->removeSharedInstance(Http::class); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_request = null; + } + /** * Data provider for testLoginPostRedirect. * @@ -798,21 +1008,6 @@ public function loginPostRedirectDataProvider() ]; } - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/customer_address.php - * @magentoAppArea frontend - */ - public function testCheckVisitorModel() - { - /** @var \Magento\Customer\Model\Visitor $visitor */ - $visitor = $this->_objectManager->get(\Magento\Customer\Model\Visitor::class); - $this->login(1); - $this->assertNull($visitor->getId()); - $this->dispatch('customer/account/index'); - $this->assertNotNull($visitor->getId()); - } - /** * @param string $email * @return void diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php index db1cc4995e676..d584fb46cda02 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php @@ -39,15 +39,6 @@ public function setUp() $this->groupRepository = $objectManager->get(\Magento\Customer\Api\GroupRepositoryInterface::class); } - /** - * @inheritDoc - */ - public function tearDown() - { - parent::tearDown(); - //$this->session->unsCustomerGroupData(); - } - /** * Test new group form. */ @@ -199,7 +190,7 @@ public function testSaveActionCreateNewGroupWithoutCode() $this->dispatch('backend/customer/group/save'); $this->assertSessionMessages( - $this->equalTo(['"code" is required. Enter and try again.']), + $this->equalTo([htmlspecialchars('"code" is required. Enter and try again.')]), MessageInterface::TYPE_ERROR ); } @@ -292,7 +283,7 @@ public function testSaveActionNewGroupWithoutGroupCode() $this->dispatch('backend/customer/group/save'); $this->assertSessionMessages( - $this->equalTo(['"code" is required. Enter and try again.']), + $this->equalTo([htmlspecialchars('"code" is required. Enter and try again.')]), MessageInterface::TYPE_ERROR ); $this->assertSessionMessages($this->isEmpty(), MessageInterface::TYPE_SUCCESS); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute.php index 516e266927179..61f92070923cd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute.php @@ -4,55 +4,37 @@ * See COPYING.txt for license details. */ -$model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); -$model->setName( - 'custom_attribute1' -)->setEntityTypeId( - 2 -)->setAttributeSetId( - 2 -)->setAttributeGroupId( - 1 -)->setFrontendInput( - 'text' -)->setFrontendLabel( - 'custom_attribute_frontend_label' -)->setIsUserDefined( - 1 -); -$model->save(); - -$model2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); -$model2->setName( - 'custom_attribute2' -)->setEntityTypeId( - 2 -)->setAttributeSetId( - 2 -)->setAttributeGroupId( - 1 -)->setFrontendInput( - 'text' -)->setFrontendLabel( - 'custom_attribute_frontend_label' -)->setIsUserDefined( - 1 -); -$model2->save(); +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Customer\Model\AttributeFactory $attributeFactory */ +$attributeFactory = $objectManager->create(\Magento\Customer\Model\AttributeFactory::class); + +/** @var \Magento\Eav\Api\AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->create(\Magento\Eav\Api\AttributeRepositoryInterface::class); /** @var \Magento\Customer\Setup\CustomerSetup $setupResource */ -$setupResource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Setup\CustomerSetup::class -); - -$data = [['form_code' => 'customer_address_edit', 'attribute_id' => $model->getAttributeId()]]; -$setupResource->getSetup()->getConnection()->insertMultiple( - $setupResource->getSetup()->getTable('customer_form_attribute'), - $data -); - -$data2 = [['form_code' => 'customer_address_edit', 'attribute_id' => $model2->getAttributeId()]]; -$setupResource->getSetup()->getConnection()->insertMultiple( - $setupResource->getSetup()->getTable('customer_form_attribute'), - $data2 -); +$setupResource = $objectManager->create(\Magento\Customer\Setup\CustomerSetup::class); + +$attributeNames = ['custom_attribute1', 'custom_attribute2']; +foreach ($attributeNames as $attributeName) { + /** @var \Magento\Customer\Model\Attribute $attribute */ + $attribute = $attributeFactory->create(); + + $attribute->setName($attributeName) + ->setEntityTypeId(2) + ->setAttributeSetId(2) + ->setAttributeGroupId(1) + ->setFrontendInput('text') + ->setFrontendLabel('custom_attribute_frontend_label') + ->setIsUserDefined(true); + + $attributeRepository->save($attribute); + + $setupResource->getSetup() + ->getConnection() + ->insertMultiple( + $setupResource->getSetup()->getTable('customer_form_attribute'), + [['form_code' => 'customer_address_edit', 'attribute_id' => $attribute->getAttributeId()]] + ); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute_rollback.php index 915b3da19783c..467356a9d914c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_user_defined_address_custom_attribute_rollback.php @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ -$model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); -$model->load('custom_attribute_test', 'attribute_code')->delete(); - -$model2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); -$model2->load('custom_attributes_test', 'attribute_code')->delete(); +/** @var \Magento\Customer\Model\Attribute $attributeModel */ +$attributeModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Customer\Model\Attribute::class +); +$attributeModel->load('custom_attribute1', 'attribute_code')->delete(); +$attributeModel->load('custom_attribute2', 'attribute_code')->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store.php new file mode 100644 index 0000000000000..79fd831dee27a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store.php @@ -0,0 +1,20 @@ +get(StoreRepositoryInterface::class); +$storeId = $storeRepository->get('fixture_second_store')->getId(); +$repository = $objectManager->create(CustomerRepositoryInterface::class); +$customer = $repository->get('customer@example.com'); +$customer->setStoreId($storeId); +$repository->save($customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store_rollback.php new file mode 100644 index 0000000000000..61cce9dbcc8d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store_rollback.php @@ -0,0 +1,8 @@ +setLastname( 'Alston' )->setGender( - 2 + '2' ); $customer->isObjectNew(true); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php index 2f9c3dc31ef3d..9b989779e4cbd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php @@ -3,16 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$customers = []; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\ObjectManagerInterface; +use Magento\Customer\Model\Customer; +use Magento\Framework\Registry; + +/** @var $objectManager ObjectManagerInterface */ +$objectManager = Bootstrap::getObjectManager(); + +$customers = []; +$customer = $objectManager->create(Customer::class); $customer->setWebsiteId( 1 -)->setEntityId( - 1 )->setEntityTypeId( 1 )->setAttributeSetId( @@ -40,13 +44,9 @@ $customer->save(); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); +$customer = $objectManager->create(Customer::class); $customer->setWebsiteId( 1 -)->setEntityId( - 2 )->setEntityTypeId( 1 )->setAttributeSetId( @@ -74,13 +74,9 @@ $customer->save(); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); +$customer = $objectManager->create(Customer::class); $customer->setWebsiteId( 1 -)->setEntityId( - 3 )->setEntityTypeId( 1 )->setAttributeSetId( @@ -108,9 +104,7 @@ $customer->save(); $customers[] = $customer; -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class) +$objectManager->get(Registry::class) ->unregister('_fixture/Magento_ImportExport_Customer_Collection'); -$objectManager->get(\Magento\Framework\Registry::class) +$objectManager->get(Registry::class) ->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php new file mode 100644 index 0000000000000..f8eeb8edd15da --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php @@ -0,0 +1,37 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $customer Customer */ +$customer = $objectManager->create(Customer::class); + +$emailsToDelete = [ + 'customer@example.com', + 'julie.worrell@example.com', + 'david.lamar@example.com', +]; +foreach ($emailsToDelete as $email) { + try { + $customer->loadByEmail($email)->delete(); + } catch (\Exception $e) { + } +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); +$registry->unregister('_fixture/Magento_ImportExport_Customer_Collection'); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/CustomerTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/CustomerTest.php index 88b748f8bbbae..884a4a38ebe0f 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/CustomerTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Export/CustomerTest.php @@ -4,80 +4,254 @@ * See COPYING.txt for license details. */ -/** - * Test for customer export model - */ namespace Magento\CustomerImportExport\Model\Export; +use Magento\Framework\Registry; +use Magento\Customer\Model\Attribute; +use Magento\ImportExport\Model\Export; +use Magento\ImportExport\Model\Import; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\ImportExport\Model\Export\Adapter\Csv; +use Magento\Customer\Model\Customer as CustomerModel; +use Magento\CustomerImportExport\Model\Export\Customer; +use Magento\Customer\Model\ResourceModel\Attribute\Collection; +use Magento\Customer\Model\ResourceModel\Customer\Collection as CustomerCollection; + +/** + * Tests for customer export model. + * + * @magentoAppArea adminhtml + */ class CustomerTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\CustomerImportExport\Model\Export\Customer + * @var Customer */ protected $_model; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var array + */ + private $attributeValues; + + /** + * @var array + */ + private $attributeTypes; + + /** + * @var Collection + */ + private $attributeCollection; + + /** + * @inheritdoc + */ protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\CustomerImportExport\Model\Export\Customer::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->_model = $this->objectManager->create(Customer::class); + $this->attributeCollection = $this->objectManager->create(Collection::class); } /** - * Test export method + * Export "Customer Main File". * * @magentoDataFixture Magento/Customer/_files/import_export/customers.php + * @return void */ public function testExport() + { + $this->processCustomerAttribute(); + $expectedAttributes = $this->getExpectedAttributes(); + $lines = $this->export($expectedAttributes); + $this->checkExportData($lines, $expectedAttributes); + } + + /** + * Return attributes which should be exported. + * + * @return array + */ + private function getExpectedAttributes(): array { $expectedAttributes = []; - /** @var $collection \Magento\Customer\Model\ResourceModel\Attribute\Collection */ - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Attribute\Collection::class - ); - /** @var $attribute \Magento\Customer\Model\Attribute */ - foreach ($collection as $attribute) { + /** @var Attribute $attribute */ + foreach ($this->attributeCollection as $attribute) { $expectedAttributes[] = $attribute->getAttributeCode(); } - $expectedAttributes = array_diff($expectedAttributes, $this->_model->getDisabledAttributes()); - $this->_model->setWriter( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\ImportExport\Model\Export\Adapter\Csv::class - ) - ); + return array_diff($expectedAttributes, $this->_model->getDisabledAttributes()); + } + + /** + * Prepare Customer attribute. + * + * @return void + */ + private function processCustomerAttribute(): void + { + $this->initAttributeValues($this->attributeCollection); + $this->initAttributeTypes($this->attributeCollection); + } + + /** + * Export customer. + * + * @param array $expectedAttributes + * @return array + */ + private function export(array $expectedAttributes): array + { + $this->_model->setWriter($this->objectManager->create(Csv::class)); $data = $this->_model->export(); + $this->assertNotEmpty($data); $lines = $this->_csvToArray($data, 'email'); - $this->assertEquals( count($expectedAttributes), count(array_intersect($expectedAttributes, $lines['header'])), - 'Expected attribute codes were not exported' + 'Expected attribute codes were not exported.' ); - $this->assertNotEmpty($lines['data'], 'No data was exported'); + $this->assertNotEmpty($lines['data'], 'No data was exported.'); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var $customers \Magento\Customer\Model\Customer[] */ - $customers = $objectManager->get( - \Magento\Framework\Registry::class - )->registry( - '_fixture/Magento_ImportExport_Customer_Collection' - ); - foreach ($customers as $key => $customer) { - foreach ($expectedAttributes as $code) { - if (!in_array($code, $this->_model->getDisabledAttributes()) && isset($lines[$key][$code])) { - $this->assertEquals( - $customer->getData($code), - $lines[$key][$code], - 'Attribute "' . $code . '" is not equal' - ); + return $lines; + } + + /** + * Check that exported data is correct. + * + * @param array $lines + * @param array $expectedAttributes + * @return void + */ + private function checkExportData(array $lines, array $expectedAttributes): void + { + /** @var CustomerModel[] $customers */ + $customers = $this->objectManager->create(CustomerCollection::class); + foreach ($customers as $customer) { + $data = $this->processCustomerData($customer, $expectedAttributes); + $exportData = $lines['data'][$data['email']]; + $exportData = $this->unsetDuplicateData($exportData); + + foreach ($data as $key => $value) { + $this->assertEquals($value, $exportData[$key], "Attribute '{$key}' is not equal."); + } + } + } + + /** + * Initialize attribute option values. + * + * @param Collection $attributeCollection + * @return CustomerTest + */ + private function initAttributeValues(Collection $attributeCollection): CustomerTest + { + /** @var Attribute $attribute */ + foreach ($attributeCollection as $attribute) { + $this->attributeValues[$attribute->getAttributeCode()] = $this->_model->getAttributeOptions($attribute); + } + + return $this; + } + + /** + * Initialize attribute types. + * + * @param \Magento\Customer\Model\ResourceModel\Attribute\Collection $attributeCollection + * @return CustomerTest + */ + private function initAttributeTypes(Collection $attributeCollection): CustomerTest + { + /** @var Attribute $attribute */ + foreach ($attributeCollection as $attribute) { + $this->attributeTypes[$attribute->getAttributeCode()] = $attribute->getFrontendInput(); + } + + return $this; + } + + /** + * Format Customer data as same as export data. + * + * @param CustomerModel $item + * @param array $expectedAttributes + * @return array + */ + private function processCustomerData(CustomerModel $item, array $expectedAttributes): array + { + $data = []; + foreach ($expectedAttributes as $attributeCode) { + $attributeValue = $item->getData($attributeCode); + + if ($this->isMultiselect($attributeCode)) { + $values = []; + $attributeValue = explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue); + foreach ($attributeValue as $value) { + $values[] = $this->getAttributeValueById($attributeCode, $value); } + $data[$attributeCode] = implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $values); + } else { + $data[$attributeCode] = $this->getAttributeValueById($attributeCode, $attributeValue); } } + + return $data; + } + + /** + * Check that attribute is multiselect type by attribute code. + * + * @param string $attributeCode + * @return bool + */ + private function isMultiselect(string $attributeCode): bool + { + return isset($this->attributeTypes[$attributeCode]) + && $this->attributeTypes[$attributeCode] === 'multiselect'; + } + + /** + * Return attribute value by id. + * + * @param string $attributeCode + * @param int|string $valueId + * @return int|string|array + */ + private function getAttributeValueById(string $attributeCode, $valueId) + { + if (isset($this->attributeValues[$attributeCode]) + && isset($this->attributeValues[$attributeCode][$valueId]) + ) { + return $this->attributeValues[$attributeCode][$valueId]; + } + + return $valueId; + } + + /** + * Unset non-useful or duplicate data from exported file data. + * + * @param array $data + * @return array + */ + private function unsetDuplicateData(array $data): array + { + unset($data['_website']); + unset($data['_store']); + unset($data['password']); + + return $data; } /** @@ -93,10 +267,7 @@ public function testGetEntityTypeCode() */ public function testGetAttributeCollection() { - $this->assertInstanceOf( - \Magento\Customer\Model\ResourceModel\Attribute\Collection::class, - $this->_model->getAttributeCollection() - ); + $this->assertInstanceOf(Collection::class, $this->_model->getAttributeCollection()); } /** @@ -104,14 +275,14 @@ public function testGetAttributeCollection() */ public function testFilterAttributeCollection() { - /** @var $collection \Magento\Customer\Model\ResourceModel\Attribute\Collection */ + /** @var $collection Collection */ $collection = $this->_model->getAttributeCollection(); $collection = $this->_model->filterAttributeCollection($collection); /** * Check that disabled attributes is not existed in attribute collection */ $existedAttributes = []; - /** @var $attribute \Magento\Customer\Model\Attribute */ + /** @var $attribute Attribute */ foreach ($collection as $attribute) { $existedAttributes[] = $attribute->getAttributeCode(); } @@ -127,7 +298,7 @@ public function testFilterAttributeCollection() * Check that all overridden attributes were affected during filtering process */ $overriddenAttributes = $this->_model->getOverriddenAttributes(); - /** @var $attribute \Magento\Customer\Model\Attribute */ + /** @var $attribute Attribute */ foreach ($collection as $attribute) { if (isset($overriddenAttributes[$attribute->getAttributeCode()])) { foreach ($overriddenAttributes[$attribute->getAttributeCode()] as $propertyKey => $property) { @@ -149,28 +320,19 @@ public function testFilterAttributeCollection() public function testFilterEntityCollection() { $createdAtDate = '2038-01-01'; - - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** * Change created_at date of first customer for future filter test. */ - $customers = $objectManager->get( - \Magento\Framework\Registry::class - )->registry( - '_fixture/Magento_ImportExport_Customer_Collection' - ); + $customers = $this->objectManager->get(Registry::class) + ->registry('_fixture/Magento_ImportExport_Customer_Collection'); $customers[0]->setCreatedAt($createdAtDate); $customers[0]->save(); /** * Change type of created_at attribute. In this case we have possibility to test date rage filter */ - $attributeCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Attribute\Collection::class - ); + $attributeCollection = $this->objectManager->create(Collection::class); $attributeCollection->addFieldToFilter('attribute_code', 'created_at'); - /** @var $createdAtAttribute \Magento\Customer\Model\Attribute */ + /** @var $createdAtAttribute Attribute */ $createdAtAttribute = $attributeCollection->getFirstItem(); $createdAtAttribute->setBackendType('datetime'); $createdAtAttribute->save(); @@ -178,19 +340,17 @@ public function testFilterEntityCollection() * Prepare filter.asd */ $parameters = [ - \Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP => [ + Export::FILTER_ELEMENT_GROUP => [ 'email' => 'example.com', 'created_at' => [$createdAtDate, ''], - 'store_id' => \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore()->getId() + 'store_id' => $this->objectManager->get(StoreManagerInterface::class)->getStore()->getId() ] ]; $this->_model->setParameters($parameters); - /** @var $customers \Magento\Customer\Model\ResourceModel\Customer\Collection */ + /** @var $customers Collection */ $collection = $this->_model->filterEntityCollection( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Customer\Collection::class + $this->objectManager->create( + CustomerCollection::class ) ); $collection->load(); @@ -223,6 +383,7 @@ protected function _csvToArray($content, $entityId = null) } } } + return $data; } } diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php index 05d9c5d3acb1e..77ceae27e0774 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php @@ -78,7 +78,7 @@ public function testImportData() $expectAddedCustomers = 5; $source = new \Magento\ImportExport\Model\Import\Source\Csv( - __DIR__ . '/_files/customers_to_import.csv', + __DIR__ . '/_files/customers_with_gender_to_import.csv', $this->directoryWrite ); @@ -133,6 +133,11 @@ public function testImportData() $updatedCustomer->getCreatedAt(), 'Creation date must be changed' ); + $this->assertEquals( + $existingCustomer->getGender(), + $updatedCustomer->getGender(), + 'Gender must be changed' + ); } /** diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv new file mode 100644 index 0000000000000..96c14c67607aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv @@ -0,0 +1,7 @@ +email,_website,_store,confirmation,created_at,created_in,default_billing,default_shipping,disable_auto_group_change,dob,firstname,gender,group_id,lastname,middlename,password_hash,prefix,rp_token,rp_token_created_at,store_id,suffix,taxvat,website_id,password +AnthonyANealy@magento.com,base,admin,,5/6/2012 15:53,Admin,1,1,0,5/6/2010,Anthony,Female,1,Nealy,A.,6a9c9bfb2ba88a6ad2a64e7402df44a763e0c48cd21d7af9e7e796cd4677ee28:RF,,,,0,,,1, +LoriBBanks@magento.com,admin,admin,,5/6/2012 15:59,Admin,3,3,0,5/6/2010,Lori,Female,1,Banks,B.,7ad6dbdc83d3e9f598825dc58b84678c7351e4281f6bc2b277a32dcd88b9756b:pz,,,,0,,,0, +CharlesTAlston@teleworm.us,base,admin,,5/6/2012 16:13,Admin,4,4,0,,Jhon,Female,1,Doe,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +customer@example.com,base,admin,,5/6/2012 16:15,Admin,4,4,0,,Firstname,Female,1,Lastname,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +julie.worrell@example.com,base,admin,,5/6/2012 16:19,Admin,4,4,0,,Julie,Female,1,Worrell,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +david.lamar@example.com,base,admin,,5/6/2012 16:25,Admin,4,4,0,,David,,1,Lamar,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, diff --git a/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php b/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php index ea155894d7299..a9591f1968ef7 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php +++ b/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php @@ -71,6 +71,7 @@ class DeployTest extends \PHPUnit\Framework\TestCase private $options = [ Options::DRY_RUN => false, Options::NO_JAVASCRIPT => false, + Options::NO_JS_BUNDLE => false, Options::NO_CSS => false, Options::NO_LESS => false, Options::NO_IMAGES => false, @@ -100,9 +101,10 @@ protected function setUp() $this->rootDir = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); $logger = $objectManager->get(\Psr\Log\LoggerInterface::class); - $this->deployService = $objectManager->create(DeployStaticContent::class, [ - 'logger' => $logger - ]); + $this->deployService = $objectManager->create( + DeployStaticContent::class, + ['logger' => $logger] + ); $this->bundleConfig = $objectManager->create(BundleConfig::class); $this->config = $objectManager->create(View::class); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index 69eab0656f89b..a6a293d384d56 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -15,12 +15,15 @@ use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Shipping\Model\Shipment\Request; use Magento\Shipping\Model\Tracking\Result\Status; +use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\HTTP\AsyncClientInterfaceMock; use Magento\Shipping\Model\Simplexml\Element as ShippingElement; /** * Test for DHL integration. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CarrierTest extends \PHPUnit\Framework\TestCase { @@ -411,7 +414,108 @@ private function getExpectedLabelRequestXml( */ public function testCollectRates() { - $requestData = [ + $requestData = $this->getRequestData(); + //phpcs:disable Magento2.Functions.DiscouragedFunction + $response = new Response( + 200, + [], + file_get_contents(__DIR__ . '/../_files/dhl_quote_response.xml') + ); + //phpcs:enable Magento2.Functions.DiscouragedFunction + $this->httpClient->nextResponses(array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response)); + /** @var RateRequest $request */ + $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $expectedRates = [ + ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 45.85, 'method' => 'E', 'price' => 45.85], + ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 35.26, 'method' => 'Q', 'price' => 35.26], + ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 37.38, 'method' => 'Y', 'price' => 37.38], + ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 35.26, 'method' => 'P', 'price' => 35.26] + ]; + + $actualRates = $this->dhlCarrier->collectRates($request)->getAllRates(); + + self::assertEquals(count($expectedRates), count($actualRates)); + foreach ($actualRates as $i => $actualRate) { + $actualRate = $actualRate->getData(); + unset($actualRate['method_title']); + self::assertEquals($expectedRates[$i], $actualRate); + } + $requestXml = $this->httpClient->getLastRequest()->getBody(); + self::assertContains('18.223', $requestXml); + self::assertContains('0.63', $requestXml); + self::assertContains('0.63', $requestXml); + self::assertContains('0.63', $requestXml); + } + + /** + * Tests that quotes request doesn't contain dimensions when it shouldn't. + * + * @param string|null $size + * @param string|null $height + * @param string|null $width + * @param string|null $depth + * @magentoConfigFixture default_store carriers/dhl/active 1 + * @dataProvider collectRatesWithoutDimensionsDataProvider + */ + public function testCollectRatesWithoutDimensions(?string $size, ?string $height, ?string $width, ?string $depth) + { + $requestData = $this->getRequestData(); + $this->setDhlConfig(['size' => $size, 'height' => $height, 'width' => $width, 'depth' => $depth]); + + /** @var RateRequest $request */ + $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); + $this->dhlCarrier = Bootstrap::getObjectManager()->create(Carrier::class); + $this->dhlCarrier->collectRates($request)->getAllRates(); + + $requestXml = $this->httpClient->getLastRequest()->getBody(); + $this->assertNotContains('', $requestXml); + $this->assertNotContains('', $requestXml); + $this->assertNotContains('', $requestXml); + + $this->config->reinit(); + } + + /** + * @return array + */ + public function collectRatesWithoutDimensionsDataProvider() + { + return [ + ['size' => '0', 'height' => '1.1', 'width' => '0.6', 'depth' => '0.7'], + ['size' => '1', 'height' => '', 'width' => '', 'depth' => ''], + ['size' => null, 'height' => '1.1', 'width' => '0.6', 'depth' => '0.7'], + ['size' => '1', 'height' => '1', 'width' => '', 'depth' => ''], + ['size' => null, 'height' => null, 'width' => null, 'depth' => null], + ]; + } + + /** + * Sets DHL config value. + * + * @param array $params + * @return void + */ + private function setDhlConfig(array $params) + { + foreach ($params as $name => $val) { + if ($val !== null) { + $this->config->setValue( + 'carriers/dhl/' . $name, + $val, + ScopeInterface::SCOPE_STORE + ); + } + } + } + + /** + * Returns request data. + * + * @return array + */ + private function getRequestData(): array + { + return [ 'data' => [ 'dest_country_id' => 'DE', 'dest_region_id' => '82', @@ -454,35 +558,5 @@ public function testCollectRates() 'all_items' => [], ] ]; - //phpcs:disable Magento2.Functions.DiscouragedFunction - $response = new Response( - 200, - [], - file_get_contents(__DIR__ . '/../_files/dhl_quote_response.xml') - ); - //phpcs:enable Magento2.Functions.DiscouragedFunction - $this->httpClient->nextResponses(array_fill(0, Carrier::UNAVAILABLE_DATE_LOOK_FORWARD + 1, $response)); - /** @var RateRequest $request */ - $request = Bootstrap::getObjectManager()->create(RateRequest::class, $requestData); - $expectedRates = [ - ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 45.85, 'method' => 'E', 'price' => 45.85], - ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 35.26, 'method' => 'Q', 'price' => 35.26], - ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 37.38, 'method' => 'Y', 'price' => 37.38], - ['carrier' => 'dhl', 'carrier_title' => 'DHL Title', 'cost' => 35.26, 'method' => 'P', 'price' => 35.26] - ]; - - $actualRates = $this->dhlCarrier->collectRates($request)->getAllRates(); - - self::assertEquals(count($expectedRates), count($actualRates)); - foreach ($actualRates as $i => $actualRate) { - $actualRate = $actualRate->getData(); - unset($actualRate['method_title']); - self::assertEquals($expectedRates[$i], $actualRate); - } - $requestXml = $this->httpClient->getLastRequest()->getBody(); - self::assertContains('18.223', $requestXml); - self::assertContains('0.630', $requestXml); - self::assertContains('0.630', $requestXml); - self::assertContains('0.630', $requestXml); } } diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php index 60c2a41fae49f..6333d60da3cfe 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php @@ -1,36 +1,59 @@ jsonSerializer = $this->_objectManager->get(Json::class); + } + /** * @inheritdoc */ protected function tearDown() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $filePath = dirname(__DIR__) . '/_files/sample.tmp'; + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (is_file($filePath)) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction unlink($filePath); } } public function testUploadAction() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction copy(dirname(__DIR__) . '/_files/sample.txt', dirname(__DIR__) . '/_files/sample.tmp'); + // phpcs:ignore Magento2.Security.Superglobal $_FILES = [ 'samples' => [ 'name' => 'sample.txt', 'type' => 'text/plain', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'tmp_name' => dirname(__DIR__) . '/_files/sample.tmp', 'error' => 0, 'size' => 0, @@ -40,7 +63,7 @@ public function testUploadAction() $this->getRequest()->setMethod('POST'); $this->dispatch('backend/admin/downloadable_file/upload/type/samples'); $body = $this->getResponse()->getBody(); - $result = Bootstrap::getObjectManager()->get(Json::class)->unserialize($body); + $result = $this->jsonSerializer->unserialize($body); $this->assertEquals(0, $result['error']); } @@ -52,9 +75,11 @@ public function testUploadAction() */ public function testUploadProhibitedExtensions($fileName) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $path = dirname(__DIR__) . '/_files/'; + // phpcs:ignore Magento2.Functions.DiscouragedFunction copy($path . 'sample.txt', $path . 'sample.tmp'); - + // phpcs:ignore Magento2.Security.Superglobal $_FILES = [ 'samples' => [ 'name' => $fileName, @@ -68,7 +93,7 @@ public function testUploadProhibitedExtensions($fileName) $this->getRequest()->setMethod('POST'); $this->dispatch('backend/admin/downloadable_file/upload/type/samples'); $body = $this->getResponse()->getBody(); - $result = Bootstrap::getObjectManager()->get(Json::class)->unserialize($body); + $result = $this->jsonSerializer->unserialize($body); self::assertArrayHasKey('errorcode', $result); self::assertEquals(0, $result['errorcode']); @@ -90,4 +115,37 @@ public function extensionsDataProvider() ['sample.php7'], ]; } + + /** + * @dataProvider uploadWrongUploadTypeDataProvider + * @return void + */ + public function testUploadWrongUploadType($postData): void + { + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod('POST'); + + $this->dispatch('backend/admin/downloadable_file/upload'); + + $body = $this->getResponse()->getBody(); + $result = $this->jsonSerializer->unserialize($body); + $this->assertEquals('Upload type can not be determined.', $result['error']); + $this->assertEquals(0, $result['errorcode']); + } + + public function uploadWrongUploadTypeDataProvider(): array + { + return [ + [ + ['type' => 'test'], + ], + [ + [ + 'type' => [ + 'type1' => 'test', + ], + ], + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Model/Url/DomainValidatorTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Model/Url/DomainValidatorTest.php new file mode 100644 index 0000000000000..86b5bf3ed05cf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Model/Url/DomainValidatorTest.php @@ -0,0 +1,98 @@ +deploymentConfig = $this->createPartialMock( + DeploymentConfig::class, + ['get'] + ); + + $domainManager = $objectManager->create( + DomainManager::class, + ['deploymentConfig' => $this->deploymentConfig] + ); + + $this->model = $objectManager->create( + DomainValidator::class, + ['domainManager' => $domainManager] + ); + } + + /** + * @param string $urlInput + * @param array $envDomainWhitelist + * @param bool $isValid + * + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture current_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture current_store web/secure/base_url https://secure.example.com/ + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://example2.com/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_url https://secure.example2.com/ + * @dataProvider isValidDataProvider + */ + public function testIsValid(string $urlInput, array $envDomainWhitelist, bool $isValid) + { + $this->deploymentConfig + ->method('get') + ->with(DomainValidator::PARAM_DOWNLOADABLE_DOMAINS) + ->willReturn($envDomainWhitelist); + + $this->assertEquals( + $isValid, + $this->model->isValid($urlInput), + 'Failed asserting is ' . ($isValid ? 'valid' : 'not valid') . ': ' . $urlInput . + PHP_EOL . + 'Domain whitelist: ' . implode(', ', $envDomainWhitelist) + ); + } + + public function isValidDataProvider() + { + return [ + ['http://example.com', ['example.co'], false], + [' http://example.com ', ['example.com'], false], + ['http://example.com', ['example.com'], true], + ['https://example.com', ['example.com'], true], + ['https://example.com/downloadable.pdf', ['example.com'], true], + ['https://example.com:8080/downloadable.pdf', ['example.com'], true], + ['http://secure.example.com', ['secure.example.com'], true], + ['https://secure.example.com', ['secure.example.com'], true], + ['https://ultra.secure.example.com', ['secure.example.com'], false], + ['http://example2.com', ['example2.com'], true], + ['https://example2.com', ['example2.com'], true], + ['http://subdomain.example2.com', ['example2.com'], false], + ['https://adobe.com', ['adobe.com'], true], + ['https://subdomain.adobe.com', ['adobe.com'], false], + ['https://ADOBE.COm', ['adobe.com'], true], + ['https://adobe.com', ['ADOBE.COm'], true], + ['http://127.0.0.1', ['127.0.0.1'], false], + ['http://[::1]', ['::1'], false], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php index 9c0b328fc1664..e312d973aeb17 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); + +use Magento\Downloadable\Api\DomainManagerInterface; $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var DomainManagerInterface $domainManager */ +$domainManager = $objectManager->get(DomainManagerInterface::class); +$domainManager->addDomains(['example.com', 'sampleurl.com']); + /** * @var \Magento\Catalog\Model\Product $product */ @@ -74,6 +79,7 @@ */ $sampleContent = $objectManager->create(\Magento\Downloadable\Api\Data\File\ContentInterfaceFactory::class)->create(); $sampleContent->setFileData( + // @codingStandardsIgnoreLine base64_encode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'test_image.jpg')) ); $sampleContent->setName('jellyfish_1_3.jpg'); @@ -92,10 +98,10 @@ */ $content = $objectManager->create(\Magento\Downloadable\Api\Data\File\ContentInterfaceFactory::class)->create(); $content->setFileData( + // @codingStandardsIgnoreLine base64_encode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'test_image.jpg')) ); $content->setName('jellyfish_2_4.jpg'); -//$content->setName(''); $sampleLink->setLinkFileContent($content); $links[] = $sampleLink; @@ -146,6 +152,7 @@ \Magento\Downloadable\Api\Data\File\ContentInterfaceFactory::class )->create(); $content->setFileData( + // @codingStandardsIgnoreLine base64_encode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'test_image.jpg')) ); $content->setName('jellyfish_1_4.jpg'); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url_rollback.php index 9ad910eed8739..48d6966fb90df 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url_rollback.php @@ -3,4 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Downloadable\Api\DomainManagerInterface; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var DomainManagerInterface $domainManager */ +$domainManager = $objectManager->get(DomainManagerInterface::class); +$domainManager->removeDomains(['sampleurl.com']); + +// @codingStandardsIgnoreLine require __DIR__ . '/product_downloadable_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable.php index 19cf449912b66..25344ea447d9a 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable.php @@ -3,10 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/** - * @var \Magento\Catalog\Model\Product $product - */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + +use Magento\Downloadable\Api\DomainManagerInterface; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var DomainManagerInterface $domainManager */ +$domainManager = $objectManager->get(DomainManagerInterface::class); +$domainManager->addDomains( + [ + 'example.com', + 'www.example.com', + 'www.sample.example.com', + 'google.com' + ] +); + +/** @var \Magento\Catalog\Model\Product $product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); $product ->setTypeId(\Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) ->setId(1) diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_rollback.php index 996fbb01d72c4..9a2e1c74fcd33 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_rollback.php @@ -3,23 +3,38 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Downloadable\Api\DomainManagerInterface; use Magento\Framework\Exception\NoSuchEntityException; \Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var DomainManagerInterface $domainManager */ +$domainManager = $objectManager->get(DomainManagerInterface::class); +$domainManager->removeDomains( + [ + 'example.com', + 'www.example.com', + 'www.sample.example.com', + 'google.com' + ] +); + /** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry = $objectManager->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() +$productRepository = $objectManager ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); try { $product = $productRepository->get('downloadable-product', false, null, true); $productRepository->delete($product); -} catch (NoSuchEntityException $e) { +} catch (NoSuchEntityException $e) { // @codingStandardsIgnoreLine } $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php index b5528dd27ee7c..f0a26f8a36d99 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_custom_options.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -use Magento\TestFramework\Helper\Bootstrap as Bootstrap; +use Magento\TestFramework\Helper\Bootstrap; require __DIR__ . '/product_downloadable_with_purchased_separately_links.php'; diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit.php new file mode 100644 index 0000000000000..f0c6e013a9d95 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit.php @@ -0,0 +1,72 @@ +get(DomainManagerInterface::class); +$domainManager->addDomains(['example.com']); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); +/** @var LinkRepositoryInterface $linkRepository */ +$linkRepository = Bootstrap::getObjectManager() + ->create(LinkRepositoryInterface::class); +/** @var ProductInterface $product */ +$product = Bootstrap::getObjectManager() + ->create(ProductInterface::class); +/** @var LinkInterface $downloadableProductLink */ +$downloadableProductLink = Bootstrap::getObjectManager() + ->create(LinkInterface::class); + +$downloadableProductLink +// ->setId(null) + ->setLinkType(Download::LINK_TYPE_URL) + ->setTitle('Downloadable Product Link') + ->setIsShareable(Link::LINK_SHAREABLE_CONFIG) + ->setLinkUrl('http://example.com/downloadable.txt') + ->setNumberOfDownloads(100) + ->setSortOrder(1) + ->setPrice(0); + +$downloadableProductLinks[] = $downloadableProductLink; + +$product + ->setId(1) + ->setTypeId(Type::TYPE_DOWNLOADABLE) + ->setExtensionAttributes( + $product->getExtensionAttributes() + ->setDownloadableProductLinks($downloadableProductLinks) + ) + ->setSku('downloadable-product') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Downloadable Product Limited') + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setLinksPurchasedSeparately(true) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit_rollback.php new file mode 100644 index 0000000000000..d88f6f1803434 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit_rollback.php @@ -0,0 +1,8 @@ +getInstance()->reinitialize(); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var DomainManagerInterface $domainManager */ +$domainManager = $objectManager->get(DomainManagerInterface::class); +$domainManager->addDomains( + [ + 'example.com', + 'www.example.com', + 'www.sample.example.com', + 'google.com' + ] +); + /** * @var \Magento\Catalog\Model\Product $product */ @@ -81,10 +95,10 @@ */ $content = $objectManager->create(\Magento\Downloadable\Api\Data\File\ContentInterfaceFactory::class)->create(); $content->setFileData( + // @codingStandardsIgnoreLine base64_encode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'test_image.jpg')) ); $content->setName('jellyfish_2_4.jpg'); -//$content->setName(''); $link->setLinkFileContent($content); /** @@ -92,6 +106,7 @@ */ $sampleContent = $objectManager->create(\Magento\Downloadable\Api\Data\File\ContentInterfaceFactory::class)->create(); $sampleContent->setFileData( + // @codingStandardsIgnoreLine base64_encode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'test_image.jpg')) ); $sampleContent->setName('jellyfish_1_3.jpg'); @@ -136,6 +151,7 @@ \Magento\Downloadable\Api\Data\File\ContentInterfaceFactory::class )->create(); $content->setFileData( + // @codingStandardsIgnoreLine base64_encode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR . 'test_image.jpg')) ); $content->setName('jellyfish_1_4.jpg'); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php index 6ceb4d90787e1..0101fb74200bc 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php @@ -5,10 +5,15 @@ */ declare(strict_types=1); -use Magento\TestFramework\Helper\Bootstrap as Bootstrap; +use Magento\TestFramework\Helper\Bootstrap; use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Downloadable\Model\Product\Type as ProductType; use Magento\Catalog\Model\Product\Visibility as ProductVisibility; +use Magento\Downloadable\Api\DomainManagerInterface; + +/** @var DomainManagerInterface $domainManager */ +$domainManager = Bootstrap::getObjectManager()->get(DomainManagerInterface::class); +$domainManager->addDomains(['example.com']); /** * @var \Magento\Catalog\Model\Product $product diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php index 308a48c144e22..5fcab9af5eb77 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php @@ -7,6 +7,11 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Downloadable\Api\DomainManagerInterface; + +/** @var DomainManagerInterface $domainManager */ +$domainManager = Bootstrap::getObjectManager()->get(DomainManagerInterface::class); +$domainManager->removeDomains(['example.com']); /** @var \Magento\Framework\Registry $registry */ $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links.php index f55261be04ce6..254e388c39037 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links.php @@ -5,10 +5,15 @@ */ declare(strict_types=1); -use Magento\TestFramework\Helper\Bootstrap as Bootstrap; +use Magento\TestFramework\Helper\Bootstrap; use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Downloadable\Model\Product\Type as ProductType; use Magento\Catalog\Model\Product\Visibility as ProductVisibility; +use Magento\Downloadable\Api\DomainManagerInterface; + +/** @var DomainManagerInterface $domainManager */ +$domainManager = Bootstrap::getObjectManager()->get(DomainManagerInterface::class); +$domainManager->addDomains(['example.com']); /** * @var \Magento\Catalog\Model\Product $product diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links_rollback.php index 4569460b10181..498fab815cdb7 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_without_purchased_separately_links_rollback.php @@ -7,6 +7,11 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Downloadable\Api\DomainManagerInterface; + +/** @var DomainManagerInterface $domainManager */ +$domainManager = Bootstrap::getObjectManager()->get(DomainManagerInterface::class); +$domainManager->removeDomains(['example.com']); /** @var \Magento\Framework\Registry $registry */ $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/Import/Product/Type/DownloadableTest.php b/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/Import/Product/Type/DownloadableTest.php index 776ec9f990f5e..15dd157a3154b 100644 --- a/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/Import/Product/Type/DownloadableTest.php +++ b/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/Import/Product/Type/DownloadableTest.php @@ -5,10 +5,12 @@ */ namespace Magento\DownloadableImportExport\Model\Import\Product\Type; +use Magento\Downloadable\Api\DomainManagerInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DownloadableTest extends \PHPUnit\Framework\TestCase { @@ -32,6 +34,11 @@ class DownloadableTest extends \PHPUnit\Framework\TestCase */ const TEST_PRODUCT_SAMPLES_GROUP_NAME = 'TEST Import Samples'; + /** + * @var DomainManagerInterface + */ + private $domainManager; + /** * @var \Magento\CatalogImportExport\Model\Import\Product */ @@ -47,6 +54,9 @@ class DownloadableTest extends \PHPUnit\Framework\TestCase */ protected $productMetadata; + /** + * @inheritDoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -56,6 +66,17 @@ protected function setUp() /** @var \Magento\Framework\EntityManager\MetadataPool $metadataPool */ $metadataPool = $this->objectManager->get(\Magento\Framework\EntityManager\MetadataPool::class); $this->productMetadata = $metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + + $this->domainManager = $this->objectManager->get(DomainManagerInterface::class); + $this->domainManager->addDomains(['www.bing.com', 'www.google.com', 'www.yahoo.com']); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + $this->domainManager->removeDomains(['www.bing.com', 'www.google.com', 'www.yahoo.com']); } /** @@ -112,7 +133,7 @@ public function testDownloadableImport() $downloadableSamples = $product->getDownloadableSamples(); //TODO: Track Fields: id, link_id, link_file and sample_file) - $expectedLinks= [ + $expectedLinks = [ 'file' => [ 'title' => 'TEST Import Link Title File', 'sort_order' => '78', @@ -154,7 +175,7 @@ public function testDownloadableImport() } //TODO: Track Fields: id, sample_id and sample_file) - $expectedSamples= [ + $expectedSamples = [ 'file' => [ 'title' => 'TEST Import Sample File', 'sort_order' => '178', diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/DateTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/DateTest.php new file mode 100644 index 0000000000000..cb75d3e0d4a8e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/DateTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\Date::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/DropDownTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/DropDownTest.php new file mode 100644 index 0000000000000..1a3f363832d6e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/DropDownTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\DropDown::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/MultipleSelectTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/MultipleSelectTest.php new file mode 100644 index 0000000000000..1c0f5ea720f70 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/MultipleSelectTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\MultipleSelect::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextAreaTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextAreaTest.php new file mode 100644 index 0000000000000..9c5b1a8587674 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextAreaTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\TextArea::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextEditorTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextEditorTest.php new file mode 100644 index 0000000000000..807e0cfd570b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextEditorTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\TextEditor::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextTest.php new file mode 100644 index 0000000000000..70069dcedd0e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/TextTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\Text::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/YesNoTest.php b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/YesNoTest.php new file mode 100644 index 0000000000000..7bb26556c3fd6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Controller/Adminhtml/Product/Attribute/Save/InputType/YesNoTest.php @@ -0,0 +1,46 @@ +createAttributeUsingDataAndAssert($attributePostData, $checkArray); + } + + /** + * Test create attribute with error. + * + * @dataProvider \Magento\TestFramework\Eav\Model\Attribute\DataProvider\YesNo::getAttributeDataWithErrorMessage() + * + * @param array $attributePostData + * @param string $errorMessage + * @return void + */ + public function testCreateAttributeWithError(array $attributePostData, string $errorMessage): void + { + $this->createAttributeUsingDataWithErrorAndAssert($attributePostData, $errorMessage); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php index c333098410800..f3d6247fa37ff 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Eav\Model; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\DataObject; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\CacheCleaner; @@ -12,7 +13,6 @@ /** * @magentoAppIsolation enabled * @magentoDbIsolation enabled - * @magentoDataFixture Magento/Eav/_files/attribute_for_search.php */ class ConfigTest extends \PHPUnit\Framework\TestCase { @@ -27,6 +27,9 @@ protected function setUp() $this->config = $objectManager->get(Config::class); } + /** + * @magentoDataFixture Magento/Eav/_files/attribute_for_search.php + */ public function testGetEntityAttributeCodes() { $entityType = 'test'; @@ -47,6 +50,9 @@ public function testGetEntityAttributeCodes() $this->assertEquals($entityAttributeCodes1, $entityAttributeCodes2); } + /** + * @magentoDataFixture Magento/Eav/_files/attribute_for_search.php + */ public function testGetEntityAttributeCodesWithObject() { $entityType = 'test'; @@ -74,6 +80,9 @@ public function testGetEntityAttributeCodesWithObject() $this->assertEquals($entityAttributeCodes1, $entityAttributeCodes2); } + /** + * @magentoDataFixture Magento/Eav/_files/attribute_for_search.php + */ public function testGetAttributes() { $entityType = 'test'; @@ -96,6 +105,9 @@ public function testGetAttributes() $this->assertEquals($attributes1, $attributes2); } + /** + * @magentoDataFixture Magento/Eav/_files/attribute_for_search.php + */ public function testGetAttribute() { $entityType = 'test'; @@ -109,4 +121,77 @@ public function testGetAttribute() $attribute2 = $this->config->getAttribute($entityType, 'attribute_for_search_1'); $this->assertEquals($attribute1, $attribute2); } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_for_caching.php + */ + public function testGetAttributeWithCacheUserDefinedAttribute() + { + /** @var MutableScopeConfigInterface $mutableScopeConfig */ + $mutableScopeConfig = Bootstrap::getObjectManager()->get(MutableScopeConfigInterface::class); + $mutableScopeConfig->setValue('dev/caching/cache_user_defined_attributes', 1); + $entityType = 'catalog_product'; + $attribute = $this->config->getAttribute($entityType, 'foo'); + $this->assertEquals('foo', $attribute->getAttributeCode()); + $this->assertEquals('foo', $attribute->getFrontendLabel()); + $this->assertEquals('varchar', $attribute->getBackendType()); + $this->assertEquals(1, $attribute->getIsRequired()); + $this->assertEquals(1, $attribute->getIsUserDefined()); + $this->assertEquals(0, $attribute->getIsUnique()); + // Update attribute + $eavSetupFactory = Bootstrap::getObjectManager()->create(\Magento\Eav\Setup\EavSetupFactory::class); + /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ + $eavSetup = $eavSetupFactory->create(); + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'foo', + [ + 'frontend_label' => 'bar', + ] + ); + // Check that attribute data has not changed + $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); + $updatedAttribute = $config->getAttribute($entityType, 'foo'); + $this->assertEquals('foo', $updatedAttribute->getFrontendLabel()); + // Clean cache + CacheCleaner::cleanAll(); + $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); + // Check that attribute data has changed + $updatedAttributeAfterCacheClean = $config->getAttribute($entityType, 'foo'); + $this->assertEquals('bar', $updatedAttributeAfterCacheClean->getFrontendLabel()); + $mutableScopeConfig->setValue('dev/caching/cache_user_defined_attributes', 0); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_for_caching.php + */ + public function testGetAttributeWithInitUserDefinedAttribute() + { + /** @var MutableScopeConfigInterface $mutableScopeConfig */ + $mutableScopeConfig = Bootstrap::getObjectManager()->get(MutableScopeConfigInterface::class); + $mutableScopeConfig->setValue('dev/caching/cache_user_defined_attributes', 0); + $entityType = 'catalog_product'; + $attribute = $this->config->getAttribute($entityType, 'foo'); + $this->assertEquals('foo', $attribute->getAttributeCode()); + $this->assertEquals('foo', $attribute->getFrontendLabel()); + $this->assertEquals('varchar', $attribute->getBackendType()); + $this->assertEquals(1, $attribute->getIsRequired()); + $this->assertEquals(1, $attribute->getIsUserDefined()); + $this->assertEquals(0, $attribute->getIsUnique()); + // Update attribute + $eavSetupFactory = Bootstrap::getObjectManager()->create(\Magento\Eav\Setup\EavSetupFactory::class); + /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ + $eavSetup = $eavSetupFactory->create(); + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'foo', + [ + 'frontend_label' => 'bar', + ] + ); + // Check that attribute data has changed + $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); + $updatedAttributeAfterCacheClean = $config->getAttribute($entityType, 'foo'); + $this->assertEquals('bar', $updatedAttributeAfterCacheClean->getFrontendLabel()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Eav/Setup/AddOptionToAttributeTest.php b/dev/tests/integration/testsuite/Magento/Eav/Setup/AddOptionToAttributeTest.php new file mode 100644 index 0000000000000..c79a4e6caddba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/Setup/AddOptionToAttributeTest.php @@ -0,0 +1,222 @@ +operation = $objectManager->get(AddOptionToAttribute::class); + /** @var ModuleDataSetupInterface $setup */ + $this->setup = $objectManager->get(ModuleDataSetupInterface::class); + /** @var AttributeRepositoryInterface attrRepo */ + $this->attrRepo = $objectManager->get(AttributeRepositoryInterface::class); + /** @var EavSetup $eavSetup */ + $this->eavSetup = $objectManager->get(EavSetupFactory::class) + ->create(['setup' => $this->setup]); + $this->attributeId = $this->eavSetup->getAttributeId(Product::ENTITY, 'zzz'); + } + + /** + * @param bool $fetchPairs + * + * @return array + */ + private function getAttributeOptions($fetchPairs = true): array + { + $optionTable = $this->setup->getTable('eav_attribute_option'); + $optionValueTable = $this->setup->getTable('eav_attribute_option_value'); + + $select = $this->setup + ->getConnection() + ->select() + ->from(['o' => $optionTable]) + ->reset('columns') + ->columns('sort_order') + ->join(['ov' => $optionValueTable], 'o.option_id = ov.option_id', 'value') + ->where(AttributeInterface::ATTRIBUTE_ID . ' = ?', $this->attributeId) + ->where('store_id = 0'); + + return $fetchPairs + ? $this->setup->getConnection()->fetchPairs($select) + : $this->setup->getConnection()->fetchAll($select); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddNewOptions() + { + $optionsBefore = $this->getAttributeOptions(false); + $this->operation->execute( + [ + 'values' => ['new1', 'new2'], + 'attribute_id' => $this->attributeId + ] + ); + $optionsAfter = $this->getAttributeOptions(false); + $this->assertEquals(count($optionsBefore) + 2, count($optionsAfter)); + $this->assertArraySubset($optionsBefore, $optionsAfter); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddExistingOptionsWithTheSameSortOrder() + { + $optionsBefore = $this->getAttributeOptions(); + $this->operation->execute( + [ + 'values' => ['Black', 'White'], + 'attribute_id' => $this->attributeId + ] + ); + $optionsAfter = $this->getAttributeOptions(); + $this->assertEquals(count($optionsBefore), count($optionsAfter)); + $this->assertArraySubset($optionsBefore, $optionsAfter); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddExistingOptionsWithDifferentSortOrder() + { + $optionsBefore = $this->getAttributeOptions(); + $this->operation->execute( + [ + 'values' => [666 => 'White', 777 => 'Black'], + 'attribute_id' => $this->attributeId + ] + ); + $optionsAfter = $this->getAttributeOptions(); + $this->assertSameSize($optionsBefore, array_intersect($optionsBefore, $optionsAfter)); + $this->assertEquals($optionsAfter[777], $optionsBefore[0]); + $this->assertEquals($optionsAfter[666], $optionsBefore[1]); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddMixedOptions() + { + $sizeBefore = count($this->getAttributeOptions()); + $this->operation->execute( + [ + 'values' => [666 => 'Black', 'NewOption'], + 'attribute_id' => $this->attributeId + ] + ); + $updatedOptions = $this->getAttributeOptions(); + $this->assertEquals(count($updatedOptions), $sizeBefore + 1); + $this->assertEquals($updatedOptions[666], 'Black'); + $this->assertEquals($updatedOptions[667], 'NewOption'); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testAddNewOption() + { + $sizeBefore = count($this->getAttributeOptions()); + + $this->operation->execute( + [ + 'attribute_id' => $this->attributeId, + 'order' => [0 => 13], + 'value' => [ + [ + 0 => 'NewOption', + ], + ], + ] + ); + $updatedOptions = $this->getAttributeOptions(); + $this->assertEquals(count($updatedOptions), $sizeBefore + 1); + $this->assertEquals($updatedOptions[13], 'NewOption'); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testDeleteOption() + { + $optionsBefore = $this->getAttributeOptions(); + $options = $this->attrRepo->get(Product::ENTITY, $this->attributeId)->getOptions(); + /** @var AttributeOptionInterface $optionToDelete */ + $optionToDelete = end($options); + $this->operation->execute( + [ + 'attribute_id' => $this->attributeId, + 'delete' => [$optionToDelete->getValue() => true], + 'value' => [ + $optionToDelete->getValue() => null, + ], + ] + ); + $updatedOptions = $this->getAttributeOptions(); + $this->assertArraySubset($updatedOptions, $optionsBefore); + $this->assertEquals(count($updatedOptions), count($optionsBefore) - 1); + } + + /** + * @magentoDataFixture Magento/Eav/_files/attribute_with_options.php + */ + public function testUpdateOption() + { + $optionsBefore = $this->getAttributeOptions(); + $this->operation->execute( + [ + 'attribute_id' => $this->attributeId, + 'value' => [ + 0 => ['updatedValue'], + ], + ] + ); + $optionsAfter = $this->getAttributeOptions(); + $this->assertEquals($optionsAfter[0], 'updatedValue'); + $this->assertSame(array_slice($optionsBefore, 1), array_slice($optionsAfter, 1)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_for_caching.php b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_for_caching.php new file mode 100644 index 0000000000000..10e8109f3f31f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_for_caching.php @@ -0,0 +1,45 @@ +create(\Magento\Eav\Model\Entity\Type::class) + ->loadByCode('catalog_product'); +$data = $entityType->getData(); +$entityTypeId = $entityType->getId(); + +/** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ +$attributeSet = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); +$attributeSet->setData( + [ + 'attribute_set_name' => 'test_attribute_set', + 'entity_type_id' => $entityTypeId, + 'sort_order' => 100 + ] +); +$attributeSet->validate(); +$attributeSet->save(); + +$attributeData = [ + [ + 'attribute_code' => 'foo', + 'entity_type_id' => $entityTypeId, + 'backend_type' => 'varchar', + 'is_required' => 1, + 'is_user_defined' => 1, + 'is_unique' => 0, + 'frontend_label' => ['foo'], + 'attribute_set_id' => $entityType->getDefaultAttributeSetId() + ] +]; + +foreach ($attributeData as $data) { + /** @var \Magento\Eav\Model\Entity\Attribute $attribute */ + $attribute = $objectManager->create(\Magento\Eav\Model\Entity\Attribute::class); + $attribute->setData($data); + $attribute->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_for_caching_rollback.php b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_for_caching_rollback.php new file mode 100644 index 0000000000000..ebc3d58028e37 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_for_caching_rollback.php @@ -0,0 +1,31 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Attribute $attribute */ +$attribute = $objectManager->create(Attribute::class); +$attribute->loadByCode(4, 'foo'); + +if ($attribute->getId()) { + $attribute->delete(); +} + +/** @var Set $attributeSet */ +$attributeSet = $objectManager->create(Set::class)->load('test_attribute_set', 'attribute_set_name'); +if ($attributeSet->getId()) { + $attributeSet->delete(); +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options.php b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options.php new file mode 100644 index 0000000000000..a53bdc68e6b1a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options.php @@ -0,0 +1,46 @@ +get(ModuleDataSetupInterface::class); +/** @var EavSetup $eavSetup */ +$eavSetup = $objectManager->get(EavSetupFactory::class) + ->create(['setup' => $setup]); +$eavSetup->addAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'zzz', + [ + 'type' => 'int', + 'backend' => '', + 'frontend' => '', + 'label' => 'zzz', + 'input' => 'select', + 'class' => '', + 'source' => '', + 'global' => 1, + 'visible' => true, + 'required' => true, + 'user_defined' => true, + 'default' => null, + 'searchable' => false, + 'filterable' => false, + 'comparable' => false, + 'visible_on_front' => false, + 'used_in_product_listing' => false, + 'unique' => true, + 'apply_to' => '', + 'system' => 1, + 'group' => 'General', + 'option' => ['values' => ["Black", "White", "Red", "Brown", "zzz", "Metallic"]] + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options_rollback.php b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options_rollback.php new file mode 100644 index 0000000000000..5b26403c797ec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Eav/_files/attribute_with_options_rollback.php @@ -0,0 +1,20 @@ +get(ModuleDataSetupInterface::class); +/** @var EavSetup $eavSetup */ +$eavSetup = $objectManager->get(EavSetupFactory::class) + ->create(['setup' => $setup]); +$eavSetup->removeAttribute(Product::ENTITY, 'zzz'); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php index fcd8226aec50c..6e21dfcab6a89 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php @@ -13,7 +13,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Indexer\Category\Product as CategoryIndexer; use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; -use Magento\Elasticsearch\Model\Config; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; @@ -35,13 +34,6 @@ protected function setUp() { parent::setUp(); - $config = $this->getMockBuilder(Config::class) - ->disableOriginalConstructor() - ->getMock(); - $config->method('isElasticsearchEnabled') - ->willReturn(true); - $this->_objectManager->addSharedInstance($config, Config::class); - $this->changeIndexerSchedule(FulltextIndexer::INDEXER_ID, true); $this->changeIndexerSchedule(CategoryIndexer::INDEXER_ID, true); } @@ -51,7 +43,6 @@ protected function setUp() */ protected function tearDown() { - $this->_objectManager->removeSharedInstance(Config::class); $this->changeIndexerSchedule(FulltextIndexer::INDEXER_ID, $this->indexerSchedule[FulltextIndexer::INDEXER_ID]); $this->changeIndexerSchedule(CategoryIndexer::INDEXER_ID, $this->indexerSchedule[CategoryIndexer::INDEXER_ID]); @@ -61,6 +52,7 @@ protected function tearDown() /** * Checks a case when indexers are invalidated if products for category were changed. * + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_category true * @magentoDataFixture Magento/Catalog/_files/category_product.php * @magentoDataFixture Magento/Catalog/_files/multiple_products.php */ @@ -161,9 +153,12 @@ private function getProductIdList(array $skuList): array $items = $repository->getList($searchCriteria) ->getItems(); - $idList = array_map(function (ProductInterface $item) { - return $item->getId(); - }, $items); + $idList = array_map( + function (ProductInterface $item) { + return $item->getId(); + }, + $items + ); return $idList; } diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch6/SearchAdapter/ConnectionManagerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch6/SearchAdapter/ConnectionManagerTest.php new file mode 100644 index 0000000000000..2b531eac4e423 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch6/SearchAdapter/ConnectionManagerTest.php @@ -0,0 +1,64 @@ +objectManager = Bootstrap::getObjectManager(); + + $this->connectionManager = $this->objectManager->create(ConnectionManager::class); + } + + /** + * Test if 'elasticsearch5' search engine returned by connection manager. + * + * @magentoAppIsolation enabled + * @magentoConfigFixture default/catalog/search/engine elasticsearch5 + */ + public function testCorrectElasticsearchClientEs5() + { + $connection = $this->connectionManager->getConnection(); + $this->assertInstanceOf( + \Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch::class, + $connection + ); + } + + /** + * Test if 'elasticsearch6' search engine returned by connection manager. + * + * @magentoAppIsolation enabled + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 + */ + public function testCorrectElasticsearchClientEs6() + { + $connection = $this->connectionManager->getConnection(); + $this->assertInstanceOf( + \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + $connection + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index 5b354dce5062f..be29846ea9f85 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -156,10 +156,11 @@ public function layoutDirectiveDataProvider() * @param $directive * @param $translations * @param $expectedResult + * @param array $variables * @internal param $translatorData * @dataProvider transDirectiveDataProvider */ - public function testTransDirective($directive, $translations, $expectedResult) + public function testTransDirective($directive, $translations, $expectedResult, $variables = []) { $renderer = Phrase::getRenderer(); @@ -168,9 +169,12 @@ public function testTransDirective($directive, $translations, $expectedResult) ->setMethods(['getData']) ->getMock(); - $translator->expects($this->atLeastOnce()) - ->method('getData') - ->will($this->returnValue($translations)); + $translator->method('getData') + ->willReturn($translations); + + if (!empty($variables)) { + $this->model->setVariables($variables); + } $this->objectManager->addSharedInstance($translator, \Magento\Framework\Translate::class); $this->objectManager->removeSharedInstance(\Magento\Framework\Phrase\Renderer\Translate::class); @@ -196,7 +200,65 @@ public function transDirectiveDataProvider() '{{trans "foobar"}}', ['foobar' => 'barfoo'], 'barfoo', - ] + ], + 'empty directive' => [ + '{{trans}}', + [], + '', + ], + 'empty string' => [ + '{{trans ""}}', + [], + '', + ], + 'no padding' => [ + '{{trans"Hello cruel coder..."}}', + [], + 'Hello cruel coder...', + ], + 'multi-line padding' => [ + "{{trans \t\n\r'Hello cruel coder...' \t\n\r}}", + [], + 'Hello cruel coder...', + ], + 'capture escaped double-quotes inside text' => [ + '{{trans "Hello \"tested\" world!"}}', + [], + 'Hello "tested" world!', + ], + 'capture escaped single-quotes inside text' => [ + "{{trans 'Hello \\'tested\\' world!'|escape}}", + [], + "Hello 'tested' world!", + ], + 'filter with params' => [ + "{{trans 'Hello \\'tested\\' world!'|escape:html}}", + [], + "Hello 'tested' world!", + ], + 'basic var' => [ + '{{trans "Hello %adjective world!" adjective="tested"}}', + [], + 'Hello tested world!', + ], + 'auto-escaped output' => [ + '{{trans "Hello %adjective world!" adjective="bad"}}', + [], + 'Hello <em>bad</em> <strong>world</strong>!', + ], + 'unescaped modifier' => [ + '{{trans "Hello %adjective world!" adjective="bad"|raw}}', + [], + 'Hello bad world!', + ], + 'variable replacement' => [ + '{{trans "Hello %adjective world!" adjective="$mood"}}', + [], + 'Hello happy world!', + [ + 'mood' => 'happy' + ], + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php index f791cdbeffe59..6ab457811999a 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php @@ -8,6 +8,7 @@ use Magento\Backend\App\Area\FrontNameResolver as BackendFrontNameResolver; use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\View\DesignInterface; use Magento\Store\Model\ScopeInterface; @@ -345,6 +346,76 @@ public function templateDirectiveDataProvider() ]; } + /** + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoComponentsDir Magento/Email/Model/_files/design + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testTemplateLoadedFromDbIsFilteredInStrictMode() + { + $this->mockModel(); + + $this->setUpThemeFallback(BackendFrontNameResolver::AREA_CODE); + + $this->model->setTemplateType(TemplateTypesInterface::TYPE_HTML); + // The first variable should be processed because it didn't come from the DB + $template = '{{var store.isSaveAllowed()}} - {{template config_path="design/email/footer_template"}}'; + $this->model->setTemplateText($template); + + // Allows for testing of templates overridden in backend + $template = $this->objectManager->create(\Magento\Email\Model\Template::class); + $templateData = [ + 'template_code' => 'some_unique_code', + 'template_type' => TemplateTypesInterface::TYPE_HTML, + // This template will be processed in strict mode + 'template_text' => '{{var this.template_code}}' + . ' - {{var store.isSaveAllowed()}} - {{var this.getTemplateCode()}}', + ]; + $template->setData($templateData); + $template->save(); + + // Store the ID of the newly created template in the system config so that this template will be loaded + $this->objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('design/email/footer_template', $template->getId(), ScopeInterface::SCOPE_STORE, 'fixturestore'); + + self::assertEquals('1 - some_unique_code - - some_unique_code', $this->model->getProcessedTemplate()); + } + + /** + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoComponentsDir Magento/Email/Model/_files/design + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testLegacyTemplateLoadedFromDbIsFilteredInLegacyMode() + { + $this->mockModel(); + + $this->setUpThemeFallback(BackendFrontNameResolver::AREA_CODE); + + $this->model->setTemplateType(TemplateTypesInterface::TYPE_HTML); + $template = '{{var store.isSaveAllowed()}} - {{template config_path="design/email/footer_template"}}'; + $this->model->setTemplateText($template); + + $template = $this->objectManager->create(\Magento\Email\Model\Template::class); + $templateData = [ + 'is_legacy' => '1', + 'template_code' => 'some_unique_code', + 'template_type' => TemplateTypesInterface::TYPE_HTML, + 'template_text' => '{{var this.template_code}}' + . ' - {{var store.isSaveAllowed()}} - {{var this.getTemplateCode()}}', + ]; + $template->setData($templateData); + $template->save(); + + // Store the ID of the newly created template in the system config so that this template will be loaded + $this->objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('design/email/footer_template', $template->getId(), ScopeInterface::SCOPE_STORE, 'fixturestore'); + + self::assertEquals('1 - some_unique_code - 1 - some_unique_code', $this->model->getProcessedTemplate()); + } + /** * Ensure that the template_styles variable contains styles from either or the "Template Styles" * textarea in backend, depending on whether template was loaded from filesystem or DB. diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php index bbbcc3133d4ba..6d5f760d7894d 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php @@ -10,9 +10,9 @@ $template->setOptions(['area' => 'test area', 'store' => 1]); $template->setData( [ - 'template_text' => - file_get_contents(__DIR__ . '/template_fixture.html'), - 'template_code' => \Magento\Theme\Model\Config\ValidatorTest::TEMPLATE_CODE + 'template_text' => file_get_contents(__DIR__ . '/template_fixture.html'), + 'template_code' => \Magento\Theme\Model\Config\ValidatorTest::TEMPLATE_CODE, + 'template_type' => \Magento\Email\Model\Template::TYPE_TEXT ] ); $template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Cache/Frontend/PoolTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Cache/Frontend/PoolTest.php new file mode 100644 index 0000000000000..3324da008a84a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Cache/Frontend/PoolTest.php @@ -0,0 +1,55 @@ +get(ObjectManagerConfig::class); + $argumentConfig = $diConfig->getArguments(\Magento\Framework\App\Cache\Frontend\Pool::class); + + $pageCacheDir = $argumentConfig['frontendSettings']['page_cache']['backend_options']['cache_dir'] ?? null; + $defaultCacheDir = $argumentConfig['frontendSettings']['default']['backend_options']['cache_dir'] ?? null; + + $noPageCacheMessage = "No default page_cache directory set in di.xml: \n" . var_export($argumentConfig, true); + $this->assertNotEmpty($pageCacheDir, $noPageCacheMessage); + + $sameCacheDirMessage = 'The page_cache and default cache storages share the same cache directory'; + $this->assertNotSame($pageCacheDir, $defaultCacheDir, $sameCacheDirMessage); + } + + /** + * @covers \Magento\Framework\App\Cache\Frontend\Pool::_getCacheSettings + * @depends testPageCacheNotSameAsDefaultCacheDirectory + */ + public function testCleaningDefaultCachePreservesPageCache() + { + $testData = 'test data'; + $testKey = 'test-key'; + + /** @var \Magento\Framework\App\Cache\Frontend\Pool $cacheFrontendPool */ + $cacheFrontendPool = ObjectManager::getInstance()->get(\Magento\Framework\App\Cache\Frontend\Pool::class); + + $pageCache = $cacheFrontendPool->get('page_cache'); + $pageCache->save($testData, $testKey); + + $defaultCache = $cacheFrontendPool->get('default'); + $defaultCache->clean(); + + $this->assertSame($testData, $pageCache->load($testKey)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Async/ProxyDeferredFactoryTest.php b/dev/tests/integration/testsuite/Magento/Framework/Async/ProxyDeferredFactoryTest.php index e4385b598c604..21392e5f7b127 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Async/ProxyDeferredFactoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Async/ProxyDeferredFactoryTest.php @@ -18,7 +18,7 @@ class ProxyDeferredFactoryTest extends TestCase { /** - * @var ProxyDeferredFactory + * @var \TestDeferred\TestClass\ProxyDeferredFactory */ private $factory; @@ -43,6 +43,7 @@ protected function setUp() //phpcs:ignore include_once __DIR__ .'/_files/test_class.php'; \TestDeferred\TestClass::$created = 0; + $this->factory = Bootstrap::getObjectManager()->get(\TestDeferred\TestClass\ProxyDeferredFactory::class); } /* @@ -57,9 +58,10 @@ public function testCreate(): void return new \TestDeferred\TestClass($value); }; /** @var \TestDeferred\TestClass $proxy */ - $proxy = $this->factory->createFor( - \TestDeferred\TestClass::class, - $this->callbackDeferredFactory->create(['callback' => $callback]) + $proxy = $this->factory->create( + [ + 'deferred' => $this->callbackDeferredFactory->create(['callback' => $callback]) + ] ); $this->assertInstanceOf(\TestDeferred\TestClass::class, $proxy); $this->assertEmpty(\TestDeferred\TestClass::$created); @@ -80,9 +82,10 @@ public function testSerialize(): void return new \TestDeferred\TestClass($value); }; /** @var \TestDeferred\TestClass $proxy */ - $proxy = $this->factory->createFor( - \TestDeferred\TestClass::class, - $this->callbackDeferredFactory->create(['callback' => $callback]) + $proxy = $this->factory->create( + [ + 'deferred' => $this->callbackDeferredFactory->create(['callback' => $callback]) + ] ); //phpcs:disable /** @var \TestDeferred\TestClass $unserialized */ @@ -106,9 +109,10 @@ public function testClone(): void return new \TestDeferred\TestClass($value); }; /** @var \TestDeferred\TestClass $proxy */ - $proxy = $this->factory->createFor( - \TestDeferred\TestClass::class, - $this->callbackDeferredFactory->create(['callback' => $callback]) + $proxy = $this->factory->create( + [ + 'deferred' => $this->callbackDeferredFactory->create(['callback' => $callback]) + ] ); $this->assertEquals(0, \TestDeferred\TestClass::$created); $this->assertEquals(0, $called); @@ -137,9 +141,10 @@ public function getValue() }; }; /** @var \TestDeferred\TestClass $proxy */ - $proxy = $this->factory->createFor( - \TestDeferred\TestClass::class, - $this->callbackDeferredFactory->create(['callback' => $callback]) + $proxy = $this->factory->create( + [ + 'deferred' => $this->callbackDeferredFactory->create(['callback' => $callback]) + ] ); $this->assertInstanceOf(\TestDeferred\TestClass::class, $proxy); $this->assertEmpty(\TestDeferred\TestClass::$created); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json index fe1d382b361fa..ed9965622dc40 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json @@ -7,7 +7,7 @@ "AFL-3.0" ], "require": { - "php": "~5.5.22|~5.6.0|~7.0.0", + "php": "~5.5.22||~5.6.0||~7.0.0", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -17,7 +17,6 @@ "ext-intl": "*", "ext-mcrypt": "*", "ext-simplexml": "*", - "ext-spl": "*", "lib-libxml": "*", "composer/composer": "1.0.0-alpha9", "magento/magento-composer-installer": "*", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php index 515e1a898dfac..0ed7bd4440be7 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php @@ -5,52 +5,136 @@ */ namespace Magento\Framework\Error; +use Magento\TestFramework\Helper\Bootstrap; + require_once __DIR__ . '/../../../../../../../pub/errors/processor.php'; class ProcessorTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\Error\Processor */ + /** + * @var Processor + */ private $processor; - public function setUp() + /** + * @inheritdoc + */ + protected function setUp() { $this->processor = $this->createProcessor(); } - public function tearDown() + /** + * {@inheritdoc} + * @throws \Exception + */ + protected function tearDown() { - if ($this->processor->reportId) { - unlink($this->processor->_reportDir . '/' . $this->processor->reportId); - } + $reportDir = $this->processor->_reportDir; + $this->removeDirRecursively($reportDir); } - public function testSaveAndLoadReport() - { + /** + * @param int $logReportDirNestingLevel + * @param int $logReportDirNestingLevelChanged + * @param string $exceptionMessage + * @dataProvider dataProviderSaveAndLoadReport + */ + public function testSaveAndLoadReport( + int $logReportDirNestingLevel, + int $logReportDirNestingLevelChanged, + string $exceptionMessage + ) { + $_ENV['MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'] = $logReportDirNestingLevel; $reportData = [ - 0 => 'exceptionMessage', + 0 => $exceptionMessage, 1 => 'exceptionTrace', 'script_name' => 'processor.php' ]; + $reportData['report_id'] = hash('sha256', implode('', $reportData)); $expectedReportData = array_merge($reportData, ['url' => '']); - $this->processor = $this->createProcessor(); - $this->processor->saveReport($reportData); - if (!$this->processor->reportId) { + $processor = $this->createProcessor(); + $processor->saveReport($reportData); + $reportId = $processor->reportId; + if (!$reportId) { $this->fail("Failed to generate report id"); } - $this->assertFileExists($this->processor->_reportDir . '/' . $this->processor->reportId); - $this->assertEquals($expectedReportData, $this->processor->reportData); + $this->assertEquals($expectedReportData, $processor->reportData); + $_ENV['MAGE_ERROR_REPORT_DIR_NESTING_LEVEL'] = $logReportDirNestingLevelChanged; + $processor = $this->createProcessor(); + $processor->loadReport($reportId); + $this->assertEquals($expectedReportData, $processor->reportData, "File contents of report don't match"); + } + + /** + * Data Provider for testSaveAndLoadReport + * + * @return array + */ + public function dataProviderSaveAndLoadReport(): array + { + return [ + [ + 'logReportDirNestingLevel' => 0, + 'logReportDirNestingLevelChanged' => 0, + 'exceptionMessage' => '$exceptionMessage 0', + ], + [ + 'logReportDirNestingLevel' => 1, + 'logReportDirNestingLevelChanged' => 1, + 'exceptionMessage' => '$exceptionMessage 1', + ], + [ + 'logReportDirNestingLevel' => 2, + 'logReportDirNestingLevelChanged' => 2, + 'exceptionMessage' => '$exceptionMessage 2', + ], + [ + 'logReportDirNestingLevel' => 3, + 'logReportDirNestingLevelChanged' => 23, + 'exceptionMessage' => '$exceptionMessage 2', + ], + [ + 'logReportDirNestingLevel' => 32, + 'logReportDirNestingLevelChanged' => 32, + 'exceptionMessage' => '$exceptionMessage 3', + ], + [ + 'logReportDirNestingLevel' => 100, + 'logReportDirNestingLevelChanged' => 100, + 'exceptionMessage' => '$exceptionMessage 100', + ], + ]; + } - $loadProcessor = $this->createProcessor(); - $loadProcessor->loadReport($this->processor->reportId); - $this->assertEquals($expectedReportData, $loadProcessor->reportData, "File contents of report don't match"); + /** + * @return Processor + */ + private function createProcessor(): Processor + { + return Bootstrap::getObjectManager()->create(Processor::class); } /** - * @return \Magento\Framework\Error\Processor + * Remove dir recursively + * + * @param string $dir + * @param int $i + * @return bool + * @throws \Exception */ - private function createProcessor() + private function removeDirRecursively(string $dir, int $i = 0): bool { - return \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Error\Processor::class); + if ($i >= 100) { + throw new \Exception('Emergency exit from recursion'); + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $i++; + (is_dir("$dir/$file")) + ? $this->removeDirRecursively("$dir/$file", $i) + : unlink("$dir/$file"); + } + return rmdir($dir); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/File/UploaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/File/UploaderTest.php new file mode 100644 index 0000000000000..15e52f5b17565 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/File/UploaderTest.php @@ -0,0 +1,101 @@ +uploaderFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\MediaStorage\Model\File\UploaderFactory::class); + + $this->filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Framework\Filesystem::class); + } + + /** + * @return void + */ + public function testUploadFileFromAllowedFolder(): void + { + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + + $fileName = 'text.txt'; + $tmpDir = 'tmp'; + $filePath = $tmpDirectory->getAbsolutePath($fileName); + + $tmpDirectory->writeFile($fileName, 'just a text'); + + $type = [ + 'tmp_name' => $filePath, + 'name' => $fileName, + ]; + + $uploader = $this->uploaderFactory->create(['fileId' => $type]); + $uploader->save($mediaDirectory->getAbsolutePath($tmpDir)); + + $this->assertTrue($mediaDirectory->isFile($tmpDir . DIRECTORY_SEPARATOR . $fileName)); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid parameter given. A valid $fileId[tmp_name] is expected. + * + * @return void + */ + public function testUploadFileFromNotAllowedFolder(): void + { + $fileName = 'text.txt'; + $tmpDir = 'tmp'; + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::LOG); + $filePath = $tmpDirectory->getAbsolutePath() . $tmpDir . DIRECTORY_SEPARATOR . $fileName; + + $tmpDirectory->writeFile($tmpDir . DIRECTORY_SEPARATOR . $fileName, 'just a text'); + + $type = [ + 'tmp_name' => $filePath, + 'name' => $fileName, + ]; + + $this->uploaderFactory->create(['fileId' => $type]); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + parent::tearDown(); + + $tmpDir = 'tmp'; + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->delete($tmpDir); + + $logDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::LOG); + $logDirectory->delete($tmpDir); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/DependDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/DependDirectiveTest.php new file mode 100644 index 0000000000000..548696f178559 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/DependDirectiveTest.php @@ -0,0 +1,84 @@ +variableResolver = $objectManager->get(StrictResolver::class); + $this->filter = $objectManager->get(Template::class); + $this->processor = $objectManager->create( + DependDirective::class, + ['variableResolver' => $this->variableResolver] + ); + } + + public function testFallbackWithNoVariables() + { + $template = 'blah {{depend foo}}blah{{/depend}} blah'; + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertEquals('{{depend foo}}blah{{/depend}}', $result); + } + + /** + * @dataProvider useCasesProvider + */ + public function testCases(string $parameter, array $variables, bool $isTrue) + { + $template = 'blah {{depend ' . $parameter . '}}blah{{/depend}} blah'; + $result = $this->processor->process( + $this->createConstruction($this->processor, $template), + $this->filter, + $variables + ); + self::assertEquals($isTrue ? 'blah' : '', $result); + } + + public function useCasesProvider() + { + return [ + ['foo',['foo' => true], true], + ['foo',['foo' => false], false], + ['foo.bar',['foo' => ['bar' => true]], true], + ['foo.bar',['foo' => ['bar' => false]], false], + ['foo.getBar().baz',['foo' => new DataObject(['bar' => ['baz' => true]])], true], + ['foo.getBar().baz',['foo' => new DataObject(['bar' => ['baz' => false]])], false], + ]; + } + + private function createConstruction(DependDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/Filter/FilterApplierTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/Filter/FilterApplierTest.php new file mode 100644 index 0000000000000..f5222c2ead1ac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/Filter/FilterApplierTest.php @@ -0,0 +1,75 @@ +applier = ObjectManager::getInstance()->get(FilterApplier::class); + } + + /** + * @dataProvider arrayUseCaseProvider + */ + public function testArrayUseCases($param, $input, $expected) + { + $result = $this->applier->applyFromArray($param, $input); + + self::assertSame($expected, $result); + } + + public function arrayUseCaseProvider() + { + $standardInput = 'Hello ' . "\n" . ' &world!'; + return [ + 'raw' => [['raw'], $standardInput, $standardInput], + 'standard usage' => [['escape', 'nl2br'], $standardInput, 'Hello
' . "\n" . ' &world!'], + 'single usage' => [['escape'], $standardInput, 'Hello ' . "\n" . ' &world!'], + 'params' => [ + ['nl2br', 'escape:url', 'foofilter'], + $standardInput, + '12%DLROW62%02%A0%E3%F2%02%RBC3%02%OLLEH' + ], + 'no filters' => [[], $standardInput, $standardInput], + 'bad filters' => [['', false, 0, null], $standardInput, $standardInput], + 'mixed filters' => [['', false, 'escape', 0, null], $standardInput, 'Hello ' . "\n" . ' &world!'], + ]; + } + + /** + * @dataProvider rawUseCaseProvider + */ + public function testRawUseCases($param, $input, $expected) + { + $result = $this->applier->applyFromRawParam($param, $input, ['escape']); + + self::assertSame($expected, $result); + } + + public function rawUseCaseProvider() + { + $standardInput = 'Hello ' . "\n" . ' &world!'; + return [ + 'raw' => ['|raw', $standardInput, $standardInput], + 'standard usage' => ['|escape|nl2br', $standardInput, 'Hello
' . "\n" . ' &world!'], + 'single usage' => ['|escape', $standardInput, 'Hello ' . "\n" . ' &world!'], + 'default filters' => ['', $standardInput, 'Hello ' . "\n" . ' &world!'], + 'params' => ['|nl2br|escape:url|foofilter', $standardInput, '12%DLROW62%02%A0%E3%F2%02%RBC3%02%OLLEH'], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/ForDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/ForDirectiveTest.php new file mode 100644 index 0000000000000..d7b268a8af6f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/ForDirectiveTest.php @@ -0,0 +1,107 @@ +variableResolver = $objectManager->get(StrictResolver::class); + $this->filter = $objectManager->get(Template::class); + $this->processor = $objectManager->create( + ForDirective::class, + ['variableResolver' => $this->variableResolver] + ); + } + + /** + * @dataProvider invalidFormatProvider + */ + public function testFallbackWithIncorrectFormat($template) + { + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertEquals($template, $result); + } + + /** + * @dataProvider useCasesProvider + */ + public function testCases(string $template, array $variables, string $expect) + { + $result = $this->processor->process( + $this->createConstruction($this->processor, $template), + $this->filter, + $variables + ); + self::assertEquals($expect, $result); + } + + public function useCasesProvider() + { + $items = [ + 'ignoreme' => [ + 'a' => 'hello1', + 'b' => ['world' => new DataObject(['foo' => 'bar1'])] + ], + [ + 'a' => 'hello2', + 'b' => ['world' => new DataObject(['foo' => 'bar2'])] + ], + ]; + $expect = '0a:hello1,b:bar11a:hello2,b:bar2'; + $body = '{{var loop.index}}a:{{var item.a}},b:{{var item.b.world.foo}}'; + + return [ + ['{{for item in foo}}' . $body . '{{/for}}',['foo' => $items], $expect], + ['{{for item in foo.bar}}' . $body . '{{/for}}',['foo' => ['bar' => $items]], $expect], + [ + '{{for item in foo.getBar().baz}}' . $body . '{{/for}}', + ['foo' => new DataObject(['bar' => ['baz' => $items]])], + $expect + ], + ]; + } + + public function invalidFormatProvider() + { + return [ + ['{{for in}}foo{{/for}}'], + ['{{for in items}}foo{{/for}}'], + ]; + } + + private function createConstruction(ForDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/IfDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/IfDirectiveTest.php new file mode 100644 index 0000000000000..1ba4412a07a54 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/IfDirectiveTest.php @@ -0,0 +1,98 @@ +variableResolver = $objectManager->get(StrictResolver::class); + $this->filter = $objectManager->get(Template::class); + $this->processor = $objectManager->create( + IfDirective::class, + ['variableResolver' => $this->variableResolver] + ); + } + + public function testFallbackWithNoVariables() + { + $template = 'blah {{if foo}}blah{{/if}} blah'; + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertEquals('{{if foo}}blah{{/if}}', $result); + } + + /** + * @dataProvider useCasesProvider + */ + public function testCases(string $template, array $variables, string $expect) + { + $result = $this->processor->process( + $this->createConstruction($this->processor, $template), + $this->filter, + $variables + ); + self::assertEquals($expect, $result); + } + + public function useCasesProvider() + { + return [ + ['{{if foo}}blah{{/if}}',['foo' => true], 'blah'], + ['{{if foo}}blah{{/if}}',['foo' => false], ''], + ['{{if foo.bar}}blah{{/if}}',['foo' => ['bar' => true]], 'blah'], + ['{{if foo.bar}}blah{{/if}}',['foo' => ['bar' => false]], ''], + ['{{if foo.getBar().baz}}blah{{/if}}',['foo' => new DataObject(['bar' => ['baz' => true]])], 'blah'], + ['{{if foo.getBar().baz}}blah{{/if}}',['foo' => new DataObject(['bar' => ['baz' => false]])], ''], + + ['{{if foo}}blah{{else}}other{{/if}}',['foo' => true], 'blah'], + ['{{if foo}}blah{{else}}other{{/if}}',['foo' => false], 'other'], + ['{{if foo.bar}}blah{{else}}other{{/if}}',['foo' => ['bar' => true]], 'blah'], + ['{{if foo.bar}}blah{{else}}other{{/if}}',['foo' => ['bar' => false]], 'other'], + [ + '{{if foo.getBar().baz}}blah{{else}}other{{/if}}', + ['foo' => new DataObject(['bar' => ['baz' => true]])], + 'blah' + ], + [ + '{{if foo.getBar().baz}}blah{{else}}other{{/if}}', + ['foo' => new DataObject(['bar' => ['baz' => false]])], + 'other' + ], + ]; + } + + private function createConstruction(IfDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/LegacyDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/LegacyDirectiveTest.php new file mode 100644 index 0000000000000..fcfcd943b603c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/LegacyDirectiveTest.php @@ -0,0 +1,69 @@ +filter = $objectManager->create(LegacyFilter::class); + $this->processor = $objectManager->create(LegacyDirective::class); + } + + public function testFallbackWithNoVariables() + { + $template = 'blah {{unknown foobar}} blah'; + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertEquals('{{unknown foobar}}', $result); + } + + /** + * @dataProvider useCaseProvider + */ + public function testCases(string $template, array $variables, string $expect) + { + $result = $this->processor->process( + $this->createConstruction($this->processor, $template), + $this->filter, + $variables + ); + self::assertEquals($expect, $result); + } + + public function useCaseProvider() + { + return [ + 'protected method' => ['{{cool "blah" foo bar baz=bash}}', [], 'value1: cool: "blah" foo bar baz=bash'], + 'public method' => ['{{cooler "blah" foo bar baz=bash}}', [], 'value2: cooler: "blah" foo bar baz=bash'], + 'simple directive' => ['{{mydir "blah" param1=bash}}foo{{/mydir}}', [], 'OOFHSABHALB'], + ]; + } + + private function createConstruction(LegacyDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php new file mode 100644 index 0000000000000..ccf867c51c3c8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php @@ -0,0 +1,105 @@ +objectManager = ObjectManager::getInstance(); + } + + public function testFallbackWhenDirectiveNotFound() + { + $filter = $this->objectManager->get(Template::class); + $processor = $this->createWithProcessorsAndFilters([], []); + + $template = 'blah {{foo bar}} blah'; + $result = $processor->process($this->createConstruction($processor, $template), $filter, []); + self::assertEquals('{{foo bar}}', $result); + } + + public function testProcessorAndFilterPoolsAreUsed() + { + $filter = $this->objectManager->create(Template::class); + + $processor = $this->createWithProcessorsAndFilters( + ['mydir' => $this->objectManager->create(MyDirProcessor::class)], + [ + 'foofilter' => $this->objectManager->create(FooFilter::class), + 'nl2br' => $this->objectManager->create(NewlineToBreakFilter::class) + ] + ); + + $template = 'blah {{mydir "somevalue" param1=yes|foofilter|nl2br|doesntexist|foofilter}}blah ' + . "\n" . '{{var address}} blah{{/mydir}} blah'; + $result = $processor->process($this->createConstruction($processor, $template), $filter, ['foo' => 'foobar']); + self::assertEquals('SOMEVALUEYESBLAH ' . "\n" .'>/ RB< BLAH', $result); + } + + public function testDefaultFiltersAreUsed() + { + $filter = $this->objectManager->create(Template::class); + + $processor = $this->createWithProcessorsAndFilters( + ['mydir' => $this->objectManager->create(MyDirProcessor::class)], + ['foofilter' => $this->objectManager->create(FooFilter::class)] + ); + + $template = 'blah {{mydir "somevalue" param1=yes}}blah ' + . "\n" . '{{var address}} blah{{/mydir}} blah'; + $result = $processor->process($this->createConstruction($processor, $template), $filter, []); + self::assertEquals('HALB ' . "\n" . ' HALBSEYEULAVEMOS', $result); + } + + public function testParametersAreParsed() + { + $filter = $this->objectManager->create(Template::class); + + $processor = $this->createWithProcessorsAndFilters( + ['mydir' => $this->objectManager->create(MyDirProcessor::class)], + ['foofilter' => $this->objectManager->create(FooFilter::class)] + ); + + $template = '{{mydir "somevalue" param1=$bar}}blah{{/mydir}}'; + $result = $processor->process($this->createConstruction($processor, $template), $filter, ['bar' => 'abc']); + self::assertEquals('HALBCBAEULAVEMOS', $result); + } + + private function createWithProcessorsAndFilters(array $processors, array $filters): SimpleDirective + { + return $this->objectManager->create( + SimpleDirective::class, + [ + 'processorPool' => $this->objectManager->create(ProcessorPool::class, ['processors' => $processors]), + 'filterPool' => $this->objectManager->create(FilterPool::class, ['filters' => $filters]), + ] + ); + } + + private function createConstruction(SimpleDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/TemplateDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/TemplateDirectiveTest.php new file mode 100644 index 0000000000000..dc6ef2cd256e8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/TemplateDirectiveTest.php @@ -0,0 +1,95 @@ +filter = $objectManager->create(Template::class); + $this->processor = $objectManager->create(TemplateDirective::class); + } + + public function testNoTemplateProcessor() + { + $template = 'blah {{template config_path="foo"}} blah'; + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertEquals('{Error in template processing}', $result); + } + + public function testNoConfigPath() + { + $this->filter->setTemplateProcessor([$this, 'processTemplate']); + $template = 'blah {{template}} blah'; + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertEquals('{Error in template processing}', $result); + } + + /** + * @dataProvider useCaseProvider + */ + public function testCases(string $template, array $variables, string $expect) + { + $this->filter->setTemplateProcessor([$this, 'processTemplate']); + $result = $this->processor->process( + $this->createConstruction($this->processor, $template), + $this->filter, + $variables + ); + self::assertEquals($expect, $result); + } + + public function useCaseProvider() + { + $prefix = '{{template config_path=$path param1=myparam '; + $expect = 'path=varpath/myparamabc/varpath'; + + return [ + [$prefix . 'varparam=$foo}}',['foo' => 'abc','path'=>'varpath'], $expect], + [$prefix . 'varparam=$foo.bar}}',['foo' => ['bar' => 'abc'],'path'=>'varpath'], $expect], + [ + $prefix . 'varparam=$foo.getBar().baz}}', + ['foo' => new DataObject(['bar' => ['baz' => 'abc']]),'path'=>'varpath'], + $expect + ], + ]; + } + + public function processTemplate(string $configPath, array $parameters) + { + // Argument + return 'path=' . $configPath + // Directive argument + . '/' . $parameters['param1'] . $parameters['varparam'] + // Template variable + . '/' . $parameters['path']; + } + + private function createConstruction(TemplateDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/VarDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/VarDirectiveTest.php new file mode 100644 index 0000000000000..7fab06178cf25 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/VarDirectiveTest.php @@ -0,0 +1,98 @@ +variableResolver = $objectManager->get(StrictResolver::class); + $this->filter = $objectManager->get(Template::class); + $this->processor = $objectManager->create( + VarDirective::class, + ['variableResolver' => $this->variableResolver] + ); + } + + public function testFallback() + { + $template = 'blah {{var}} blah'; + $result = $this->processor->process($this->createConstruction($this->processor, $template), $this->filter, []); + self::assertSame('{{var}}', $result); + } + + /** + * @dataProvider useCasesProvider + */ + public function testCases(string $parameter, array $variables, string $expect) + { + $template = 'blah {{var ' . $parameter . '}} blah'; + $result = $this->processor->process( + $this->createConstruction($this->processor, $template), + $this->filter, + $variables + ); + self::assertEquals($expect, $result); + } + + public function useCasesProvider() + { + return [ + ['foo',['foo' => true], '1'], + ['foo',['foo' => 'abc'], 'abc'], + ['foo',['foo' => 1.234], '1.234'], + ['foo',['foo' => 0xF], '15'], + ['foo',['foo' => false], ''], + ['foo',['foo' => null], ''], + ['foo.bar',['foo' => ['bar' => 'abc']], 'abc'], + ['foo.bar',['foo' => ['bar' => false]], ''], + ['foo.getBar().baz',['foo' => new DataObject(['bar' => ['baz' => 'abc']])], 'abc'], + ['foo.getBar().baz',['foo' => new DataObject(['bar' => ['baz' => false]])], ''], + [ + 'foo.getBar().baz|foofilter|nl2br', + ['foo' => new DataObject(['bar' => ['baz' => "foo\nbar"]])], + "RAB
\nOOF" + ], + [ + 'foo.getBar().baz|foofilter:myparam|nl2br|doesntexist|nl2br', + ['foo' => new DataObject(['bar' => ['baz' => "foo\nbar"]])], + "MARAPYMRAB

\nOOF" + ], + ]; + } + + private function createConstruction(VarDirective $directive, string $value): array + { + preg_match($directive->getRegularExpression(), $value, $construction); + + return $construction; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php index 71e4e438d945d..de09b87b04e4a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php @@ -5,6 +5,10 @@ */ namespace Magento\Framework\Filter; +use Magento\Framework\DataObject; +use Magento\Store\Model\Store; +use Magento\TestFramework\ObjectManager; + class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -14,26 +18,26 @@ class TemplateTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->templateFilter = \Magento\TestFramework\ObjectManager::getInstance()->create(Template::class); + $this->templateFilter = ObjectManager::getInstance()->create(Template::class); } /** * @param array $results - * @param array $values + * @param array $value * @dataProvider getFilterForDataProvider */ - public function testFilterFor($results, $values) + public function testFilterFor($results, $value) { $this->templateFilter->setVariables(['order' => $this->getOrder(), 'things' => $this->getThings()]); - $this->assertEquals($results, $this->invokeMethod($this->templateFilter, 'filterFor', [$values])); + self::assertEquals($results, $this->templateFilter->filter($value)); } /** - * @return \Magento\Framework\DataObject + * @return DataObject */ private function getOrder() { - $order = new \Magento\Framework\DataObject(); + $order = new DataObject(); $visibleItems = [ [ 'sku' => 'ABC123', @@ -121,20 +125,256 @@ public function getFilterForDataProvider() ]; } + public function testDependDirective() + { + $this->templateFilter->setVariables( + [ + 'customer' => new DataObject(['name' => 'John Doe']), + ] + ); + + $template = '{{depend customer.getName()}}foo{{/depend}}'; + $template .= '{{depend customer.getName()}}{{var customer.getName()}}{{/depend}}'; + $template .= '{{depend customer.getFoo()}}bar{{/depend}}'; + $expected = 'fooJohn Doe'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testIfDirective() + { + $this->templateFilter->setVariables( + [ + 'customer' => new DataObject(['name' => 'John Doe']), + ] + ); + + $template = '{{if customer.getName()}}foo{{/if}}{{if customer.getNope()}}not me{{else}}bar{{/if}}'; + $expected = 'foobar'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testNonDataObjectVariableParsing() + { + $this->templateFilter->setVariables( + [ + 'address' => new class { + public function format($type) + { + return '' . $type . ''; + } + } + ] + ); + + $template = '{{var address.format(\'html\')}}'; + $expected = 'html'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testComplexVariableArguments() + { + $this->templateFilter->setVariables( + [ + 'address' => new class { + public function format($a, $b, $c) + { + return $a . ' ' . $b . ' ' . $c['param1']; + } + }, + 'arg1' => 'foo' + ] + ); + + $template = '{{var address.format($arg1,\'bar\',[param1:baz])}}'; + $expected = 'foo bar baz'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testComplexVariableGetterArguments() + { + $this->templateFilter->setVariables( + [ + 'address' => new class extends DataObject { + public function getFoo($a, $b, $c) + { + return $a . ' ' . $b . ' ' . $c['param1']; + } + }, + 'arg1' => 'foo' + ] + ); + + $template = '{{var address.getFoo($arg1,\'bar\',[param1:baz])}}'; + $expected = 'foo bar baz'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testNonDataObjectRendersBlankInStrictMode() + { + $this->templateFilter->setStrictMode(true); + $this->templateFilter->setVariables( + [ + 'address' => new class { + public function format($type) + { + return '' . $type . ''; + } + }, + ] + ); + + $template = '{{var address.format(\'html\')}}'; + $expected = ''; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testDataObjectCanRenderPropertiesStrictMode() + { + $this->templateFilter->setStrictMode(true); + $this->templateFilter->setVariables( + [ + 'customer' => new DataObject(['name' => 'John Doe']), + ] + ); + + $template = '{{var customer.name}} - {{var customer.getName()}}'; + $expected = 'John Doe - John Doe'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + /** + * @dataProvider strictModeTrueFalseProvider + */ + public function testScalarDataKeys($strictMode) + { + $this->templateFilter->setStrictMode($strictMode); + $this->templateFilter->setVariables( + [ + 'customer_data' => [ + 'name' => 'John Doe', + 'address' => [ + 'street' => ['easy'], + 'zip' => new DataObject(['bar' => 'yay']) + ] + ], + 'myint' => 123, + 'myfloat' => 1.23, + 'mystring' => 'abc', + 'mybool' => true, + 'myboolf' => false, + ] + ); + + $template = '{{var customer_data.name}}' + . ' {{var customer_data.address.street.0}}' + . ' {{var customer_data.address.zip.bar}}' + . ' {{var}}' + . ' {{var myint}}' + . ' {{var myfloat}}' + . ' {{var mystring}}' + . ' {{var mybool}}' + . ' {{var myboolf}}'; + + $expected = 'John Doe easy yay {{var}} 123 1.23 abc 1 '; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testModifiers() + { + $this->templateFilter->setVariables( + [ + 'address' => '11501 Domain Dr.' . "\n" . 'Austin, TX 78758' + ] + ); + + $template = '{{mydir "somevalue" param1=yes|foofilter|nl2br}}blah {{var address}} blah{{/mydir}}'; + + $expected = 'HALB 85787 XT ,NITSUA
' . "\n" . '.RD NIAMOD 10511 HALBSEYEULAVEMOS'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testDefaultModifiers() + { + $this->templateFilter->setVariables( + [ + 'address' => '11501 Domain Dr.' . "\n" . 'Austin, TX 78758' + ] + ); + + $template = '{{mydir "somevalue" param1=yes}}blah {{var address}} blah{{/mydir}}'; + + $expected = 'HALB 85787 XT ,NITSUA' . "\n" . '.RD NIAMOD 10511 HALBSEYEULAVEMOS'; + self::assertEquals($expected, $this->templateFilter->filter($template)); + } + + public function testFilterVarious1() + { + $this->templateFilter->setVariables( + [ + 'customer' => new DataObject(['firstname' => 'Felicia', 'lastname' => 'Henry']), + 'company' => 'A. L. Price', + 'street1' => '687 Vernon Street', + 'city' => 'Parker Dam', + 'region' => 'CA', + 'postcode' => '92267', + 'telephone' => '760-663-5876', + ] + ); + + $template = <<